Source code for pybosl2.shapes3d.cylinder

# 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/shapes3d/cylinder.py
# FileSummary: Cylinders, cones, shear cylinders and pipe shapes.
# DocCategory: Foundational
# FileGroup: BOSL2

"""Cylinders, cones, shear cylinders and pipe shapes."""

from __future__ import annotations

import math
from typing import TYPE_CHECKING

from pybosl2._backend import backend_only
from pybosl2._edges_lang import Anchor
from pybosl2._native import native

if TYPE_CHECKING:
    from collections.abc import Sequence

    from pybosl2.textures import TextureType

from pybosl2._helpers import frag_count as _frag_count
from pybosl2._helpers import pick_radius as _pick_radius
from pybosl2._helpers import quantup
from pybosl2.constants import BOTTOM, CENTER
from pybosl2.exceptions import Bosl2ValueError
from pybosl2.groups import resolve_center_anchor

# Import base class and helper functions from shapes3d.base
from .base import (
    Bosl2Solid,
    _anchor_offset_cyl,
    _finish3,
    _ocylinder,
    _osphere,
)

if TYPE_CHECKING:  # real stub-typed imports for the checker (identical to pre-lazy)
    from pythonscad import cube as _ocube
    from pythonscad import cylinder as _ocylinder_native
    from pythonscad import hull as _ohull
    from pythonscad import minkowski as _ominkowski
    from pythonscad import polyhedron as _opolyhedron
    from pythonscad import rotate_extrude as _orotate_extrude
    from pythonscad import sphere as _osphere_native
    from pythonscad import textmetrics as _otextmetrics
else:
    _ocube = native("cube")
    _ocylinder_native = native("cylinder")
    _ohull = native("hull")
    _ominkowski = native("minkowski")
    _opolyhedron = native("polyhedron")
    _orotate_extrude = native("rotate_extrude")
    _osphere_native = native("sphere")
    _otextmetrics = native("textmetrics")


# ---------------------------------------------------------------------------
# Section: Cylinders
# ---------------------------------------------------------------------------


[docs] @backend_only("csg", neutral="pybosl2.solid.cylinder") def cylinder( height: float | None = None, radius: float | None = None, center: bool | None = None, length: float | None = None, radius1: float | None = None, radius2: float | None = None, diameter: float | None = None, diameter1: float | None = None, diameter2: float | None = None, chamfer: float | None = None, chamfer1: float | None = None, chamfer2: float | None = None, rounding: float | None = None, rounding1: float | None = None, rounding2: float | None = None, circumscribe: bool = False, realign: bool = False, shift: Sequence[float] = (0, 0), anchor: Anchor | Sequence[float] | None = None, spin: float = 0, orient: Anchor | Sequence[float] = Anchor.TOP, fn: int | None = None, fa: float | None = None, fs: float | None = None, chamfer_angle: float | None = None, chamfer_angle1: float | None = None, chamfer_angle2: float | None = None, from_end: bool = False, from_end1: bool | None = None, from_end2: bool | None = None, extra: float = 0.0, extra1: float | None = None, extra2: float | None = None, teardrop: float | bool = False, clip_angle: float = 90.0, texture: str | TextureType | None = None, tex_size: float | Sequence[float] | None = None, tex_reps: int | Sequence[int] | None = None, tex_depth: float = 1.0, tex_inset: float | bool = False, ) -> Bosl2Solid: """Return a cylinder with optional chamfering/rounding of its end rims, built with. cube()/cylinder()/sphere()/rotate_extrude(). Positive rounding is built as a minkowski() of a shorter cylinder with a sphere at each rounded end (an inset fillet, not an outward bulge), matching BOSL2's own rounded-end geometry. Chamfering builds the exact half-profile (with the requested bevel at each end) and revolves it with rotate_extrude(). Note: ``texture=`` (VNF surface texturing) is not supported by this pure-Python port. Args: length: length of the cylinder along its axis (default 1) height: length of the cylinder along its axis (default 1) radius: radius of the cylinder (default 1) diameter: diameter of the cylinder radius1: radius of the negative end of the cylinder radius2: radius of the positive end of the cylinder diameter1: diameter of the negative end of the cylinder diameter2: diameter of the positive end of the cylinder center: if given, overrides anchor (True -> CENTER, False -> BOTTOM) chamfer: chamfer size on the end rims (overall/negative/positive) chamfer1: chamfer size on the end rims (overall/negative/positive) chamfer2: chamfer size on the end rims (overall/negative/positive) rounding: rounding radius on the end rims (overall/negative/positive) rounding1: rounding radius on the end rims (overall/negative/positive) rounding2: rounding radius on the end rims (overall/negative/positive) circumscribe: circumscribe rather than inscribe the given radius (default False) realign: shift point alignment (default False) shift: X/Y offset for the positive end (shear) (default [0,0]) anchor: anchor point (default BOTTOM if center=False, otherwise CENTER) spin: Z-axis rotation in degrees after anchor (default 0) orient: direction to rotate the top towards, after spin (default UP) fn: arc smoothness overrides. Omitted, the ambient ``use_defaults(fn=...)`` value applies; ``fn=0`` opts back out to fa/fs. fa: arc smoothness overrides. Omitted, the ambient ``use_defaults(fa=...)`` value applies. fs: arc smoothness overrides. Omitted, the ambient ``use_defaults(fs=...)`` value applies. chamfer_angle: chamfer angle in degrees away from ends chamfer_angle1: chamfer angle in degrees away from ends chamfer_angle2: chamfer angle in degrees away from ends from_end: measure chamfer along conic face (default False) from_end1: measure chamfer along conic face (default False) from_end2: measure chamfer along conic face (default False) extra: add extra height at ends (invisible to anchoring) extra1: add extra height at ends (invisible to anchoring) extra2: add extra height at ends (invisible to anchoring) teardrop: limit rounding angle from horizontal clip_angle: clip rounding arc at bottom of cylinder texture: named texture to apply to cylinder side surface tex_size: size of texture tile tex_reps: number of texture repetitions tex_depth: depth of the texture tex_inset: inset the texture Examples: A basic cylinder: .. pythonscad-example:: from pybosl2.solid import cylinder cylinder(height=30, radius=10).show() A cylinder with chamfered ends: .. pythonscad-example:: from pybosl2.solid import cylinder cylinder(height=40, radius=15, chamfer=2).show() A cylinder with rounded ends: .. pythonscad-example:: from pybosl2.solid import cylinder cylinder(height=30, radius=12, rounding=2).show() """ return cyl( height=height, radius=radius, center=center, length=length, radius1=radius1, radius2=radius2, diameter=diameter, diameter1=diameter1, diameter2=diameter2, chamfer=chamfer, chamfer1=chamfer1, chamfer2=chamfer2, rounding=rounding, rounding1=rounding1, rounding2=rounding2, circumscribe=circumscribe, realign=realign, shift=list(shift), anchor=anchor, spin=spin, orient=orient, fn=fn, fa=fa, fs=fs, chamfer_angle=chamfer_angle, chamfer_angle1=chamfer_angle1, chamfer_angle2=chamfer_angle2, from_end=from_end, from_end1=from_end1, from_end2=from_end2, extra=extra, extra1=extra1, extra2=extra2, teardrop=teardrop, clip_angle=clip_angle, texture=texture, tex_size=tex_size, tex_reps=tex_reps, tex_depth=tex_depth, tex_inset=tex_inset, )
@backend_only("csg", neutral="pybosl2.solid.cyl") def _textured_cyl( length: float, radius1: float, radius2: float, tex: "str | TextureType", *, tex_size: "float | Sequence[float] | None", tex_reps: "int | Sequence[int] | None", tex_depth: float, tex_inset: float | bool, anchor: "Anchor | Sequence[float]", spin: float, orient: "Anchor | Sequence[float]", fn: int | None, fa: float | None, fs: float | None, ) -> Bosl2Solid: """Build a cylinder whose side carries *tex*, as a polyhedron (SPEC S-34, S-35). The mesh is built in pure Python (:func:`~pybosl2.textures.textured_cylinder_vnf`) and crosses to geometry once, at the end. When neither *tex_size* nor *tex_reps* is given the repeats are derived from the facet count the cylinder would have had, so a textured cylinder is as smooth as the plain one it replaces. Args: length: Height of the cylinder. radius1: Radius at the bottom. radius2: Radius at the top. tex: The texture, by name or already built. tex_size: Size of one tile in millimetres. tex_reps: Repeat counts, instead of *tex_size*. tex_depth: How far the texture displaces the surface. tex_inset: How far the surface is sunk before the texture is added. anchor: Anchor point. spin: Z-axis rotation in degrees after anchor. orient: Direction to rotate the top towards, after spin. fn: Fixed fragment count, used to derive the repeats when none are given. fa: Minimum fragment angle in degrees. fs: Minimum fragment size in millimetres. Returns: The textured cylinder. """ from pybosl2.textures import default_tex_reps, textured_cylinder_vnf if tex_size is None and tex_reps is None: tex_reps = default_tex_reps(length, radius1, radius2) mesh = textured_cylinder_vnf( length, radius1, radius2, tex, tex_size=tex_size, tex_reps=tex_reps, tex_depth=tex_depth, tex_inset=tex_inset, fn=fn, fa=fa, fs=fs, ) solid = mesh.polyhedron() offset = _anchor_offset_cyl(radius1, radius2, length, anchor) return _finish3(Bosl2Solid._unwrap(solid), offset, spin, orient) def _cyl_anchor(center: bool | None, anchor: "Anchor | Sequence[float] | None") -> "Anchor | Sequence[float]": """Resolve `center=`/`anchor=` for the cylinder family, which centres when given neither. Args: center: The `center=` shorthand as passed. anchor: The anchor as passed, or ``None``. Returns: The anchor to place with -- ``CENTER`` when the caller named neither (SPEC D-4). """ resolved = resolve_center_anchor(center=center, anchor=anchor, centred=Anchor.CENTER, uncentred=BOTTOM) return CENTER if resolved is None else resolved
[docs] def cyl( height: float | None = None, radius: float | None = None, center: bool | None = None, length: float | None = None, radius1: float | None = None, radius2: float | None = None, diameter: float | None = None, diameter1: float | None = None, diameter2: float | None = None, chamfer: float | None = None, chamfer1: float | None = None, chamfer2: float | None = None, rounding: float | None = None, rounding1: float | None = None, rounding2: float | None = None, circumscribe: bool = False, realign: bool = False, shift: Sequence[float] = (0, 0), anchor: Anchor | Sequence[float] | None = None, spin: float = 0, orient: Anchor | Sequence[float] = Anchor.TOP, fn: int | None = None, fa: float | None = None, fs: float | None = None, # Additional missing args chamfer_angle: float | None = None, chamfer_angle1: float | None = None, chamfer_angle2: float | None = None, from_end: bool = False, from_end1: bool | None = None, from_end2: bool | None = None, extra: float = 0.0, extra1: float | None = None, extra2: float | None = None, teardrop: float | bool = False, clip_angle: float = 90.0, texture: str | TextureType | None = None, tex_size: float | Sequence[float] | None = None, tex_reps: int | Sequence[int] | None = None, tex_depth: float = 1.0, tex_inset: float | bool = False, ) -> Bosl2Solid: """Return a cylinder with optional chamfering/rounding of its end rims, built with. cube()/cylinder()/sphere()/rotate_extrude(). Positive rounding is built as a minkowski() of a shorter cylinder with a sphere at each rounded end (an inset fillet, not an outward bulge), matching BOSL2's own rounded-end geometry. Chamfering builds the exact half-profile (with the requested bevel at each end) and revolves it with rotate_extrude(). Note: `texture=` (VNF surface texturing) is not supported by this pure-Python port. Args: length: length of the cylinder along its axis (default 1) height: length of the cylinder along its axis (default 1) radius: radius of the cylinder (default 1) diameter: diameter of the cylinder radius1: radius of the negative end of the cylinder radius2: radius of the positive end of the cylinder diameter1: diameter of the negative end of the cylinder diameter2: diameter of the positive end of the cylinder center: if given, overrides anchor (True -> CENTER, False -> BOTTOM) chamfer: chamfer size on the end rims (overall/negative/positive) chamfer1: chamfer size on the end rims (overall/negative/positive) chamfer2: chamfer size on the end rims (overall/negative/positive) rounding: rounding radius on the end rims (overall/negative/positive) rounding1: rounding radius on the end rims (overall/negative/positive) rounding2: rounding radius on the end rims (overall/negative/positive) circumscribe: circumscribe rather than inscribe the given radius (default False) realign: shift point alignment (default False) shift: X/Y offset for the positive end (shear) (default [0,0]) anchor: anchor point (default CENTER) spin: Z-axis rotation in degrees after anchor (default 0) orient: direction to rotate the top towards, after spin (default UP) fn: arc smoothness overrides. Omitted, the ambient ``use_defaults(fn=...)`` value applies; ``fn=0`` opts back out to fa/fs. fa: arc smoothness overrides. Omitted, the ambient ``use_defaults(fa=...)`` value applies. fs: arc smoothness overrides. Omitted, the ambient ``use_defaults(fs=...)`` value applies. chamfer_angle: chamfer angle in degrees away from ends chamfer_angle1: chamfer angle in degrees away from ends chamfer_angle2: chamfer angle in degrees away from ends from_end: measure chamfer along conic face (default False) from_end1: measure chamfer along conic face (default False) from_end2: measure chamfer along conic face (default False) extra: add extra height at ends (invisible to anchoring) extra1: add extra height at ends (invisible to anchoring) extra2: add extra height at ends (invisible to anchoring) teardrop: limit rounding angle from horizontal clip_angle: clip rounding arc at bottom of cylinder texture: named texture to apply to cylinder side surface tex_size: size of texture tile tex_reps: number of texture repetitions tex_depth: depth of the texture tex_inset: inset the texture Examples: A basic cylinder: .. pythonscad-example:: from pybosl2 import cyl shape = cyl(radius=10, height=30) shape.show() A cylinder with chamfered ends: .. pythonscad-example:: from pybosl2 import cyl shape = cyl(radius=15, height=40, chamfer=2) shape.show() A cylinder with rounded ends: .. pythonscad-example:: from pybosl2 import cyl shape = cyl(radius=12, height=35, rounding=3) shape.show() """ length_val = next((v for v in (length, height) if v is not None), 1.0) rad1 = _pick_radius(radius1=radius1, diameter1=diameter1, radius=radius, diameter=diameter, dflt=1) rad2 = _pick_radius(radius2=radius2, diameter2=diameter2, radius=radius, diameter=diameter, dflt=1) if circumscribe: sides = _frag_count(max(rad1, rad2), fn, fa, fs) sc = 1 / math.cos(math.pi / sides) rad1 *= sc rad2 *= sc use_anchor = _cyl_anchor(center, anchor) if texture is not None and texture != "none": return _textured_cyl( length_val, rad1, rad2, texture, tex_size=tex_size, tex_reps=tex_reps, tex_depth=tex_depth, tex_inset=tex_inset, anchor=use_anchor, spin=spin, orient=orient, fn=fn, fa=fa, fs=fs, ) r1v = rounding1 if rounding1 is not None else (rounding if rounding is not None else 0) r2v = rounding2 if rounding2 is not None else (rounding if rounding is not None else 0) c1v = chamfer1 if chamfer1 is not None else (chamfer if chamfer is not None else 0) c2v = chamfer2 if chamfer2 is not None else (chamfer if chamfer is not None else 0) if (r1v or r2v) and (c1v or c2v): raise Bosl2ValueError("Cannot specify nonzero value for both chamfer and rounding") _check_rim_treatments(rad1, rad2, r1v, r2v, c1v, c2v) cfang1 = chamfer_angle1 if chamfer_angle1 is not None else (chamfer_angle if chamfer_angle is not None else None) cfang2 = chamfer_angle2 if chamfer_angle2 is not None else (chamfer_angle if chamfer_angle is not None else None) fe1 = from_end1 if from_end1 is not None else from_end fe2 = from_end2 if from_end2 is not None else from_end if not (r1v or r2v or c1v or c2v): shape = _ocylinder( height=length_val, radius1=rad1, radius2=rad2, center=True, fn=fn, fa=fa, fs=fs, ) elif ( rad1 == rad2 and r1v == r2v and r1v > 0 and not c1v and not c2v and (teardrop is False or teardrop is None) and clip_angle == 90.0 ): # Straight cylinder, uniform rounding on both ends: exact via minkowski(cylinder, sphere). inner_r = max(0.001, rad1 - r1v) inner_l = max(0.001, length_val - 2 * r1v) sphere_fn = int(quantup(_frag_count(r1v, fn, fa, fs), 4)) shape = _ominkowski( _ocylinder(height=inner_l, radius=inner_r, center=True, fn=fn, fa=fa, fs=fs), _osphere(radius=r1v, fn=sphere_fn), ) else: profile = cyl_profile( rad1, rad2, length_val, rounding1=r1v, rounding2=r2v, chamfer1=c1v, chamfer2=c2v, chamfer_angle1=cfang1, chamfer_angle2=cfang2, from_end1=fe1, from_end2=fe2, fn=fn, fa=fa, fs=fs, teardrop=teardrop, clip_angle=clip_angle, ) from pybosl2._native import native _opolygon = native("polygon") shape = _orotate_extrude(_opolygon(profile), fn=fn, fa=fa, fs=fs) if realign: sides = _frag_count(max(rad1, rad2), fn, fa, fs) shape = shape.rotate(180 / sides, [0, 0, 1]) if shift[0] or shift[1]: shear = [ [1, 0, shift[0] / length_val, 0], [0, 1, shift[1] / length_val, 0], [0, 0, 1, 0], [0, 0, 0, 1], ] shape = shape.multmatrix(shear) extra1_val = extra1 if extra1 is not None else extra extra2_val = extra2 if extra2 is not None else extra if extra1_val > 0: ext1 = _ocylinder(height=extra1_val, radius=rad1, center=False, fn=fn, fa=fa, fs=fs).translate( [0, 0, -length_val / 2 - extra1_val] ) shape = shape | ext1 if extra2_val > 0: ext2 = _ocylinder(height=extra2_val, radius=rad2, center=False, fn=fn, fa=fa, fs=fs).translate( [0, 0, length_val / 2] ) shape = shape | ext2 offset = _anchor_offset_cyl(rad1, rad2, length_val, use_anchor) return _finish3(shape, offset, spin, orient, size=None, anchor=use_anchor)
def _check_rim_treatments( radius1: float, radius2: float, rounding1: float, rounding2: float, chamfer1: float, chamfer2: float, ) -> None: """Reject a rim rounding/chamfer too big for the rim it sits on. A treatment bigger than its end's radius pushes the revolved profile past the axis, and ``rotate_extrude()`` refuses children that cross it -- which used to surface as a raw OpenSCAD error plus a solid with no bounding box. A cone's top radius is 0, so *any* top treatment did this: ``cone(height=30, radius=15, chamfer=1)`` produced nothing usable (SPEC E-4). Args: radius1: Radius at the bottom end. radius2: Radius at the top end. rounding1: Rounding radius at the bottom rim. rounding2: Rounding radius at the top rim. chamfer1: Chamfer size at the bottom rim. chamfer2: Chamfer size at the top rim. Raises: ValueError: If a treatment exceeds the radius of the end it is applied to. """ for label, treatment, radius in ( ("rounding1", rounding1, radius1), ("rounding2", rounding2, radius2), ("chamfer1", chamfer1, radius1), ("chamfer2", chamfer2, radius2), ): if treatment and treatment > radius: raise Bosl2ValueError( f"cyl(): {label}={treatment} is larger than that end's radius ({radius}); " f"a rim treatment has to fit inside the rim it rounds." )
[docs] def cyl_profile( radius1: float, radius2: float, length: float, rounding1: float = 0, rounding2: float = 0, chamfer1: float = 0, chamfer2: float = 0, chamfer_angle1: float | None = None, chamfer_angle2: float | None = None, from_end1: bool = False, from_end2: bool = False, fn: int | None = None, fa: float | None = None, fs: float | None = None, teardrop: float | bool = False, clip_angle: float = 90.0, ) -> list[list[float]]: """Generate a 2D cylinder profile with optional rounding and chamfering. Args: radius1: Radius at the bottom end. radius2: Radius at the top end. length: Length of the cylinder along its axis. rounding1: Rounding radius at the bottom end; negative rounds outward. rounding2: Rounding radius at the top end; negative rounds outward. chamfer1: Chamfer size at the bottom end. chamfer2: Chamfer size at the top end. chamfer_angle1: Angle of the bottom chamfer in degrees, measured from the end face. chamfer_angle2: Angle of the top chamfer in degrees, measured from the end face. from_end1: Measure the bottom chamfer along the end face rather than up the side. from_end2: Measure the top chamfer along the end face rather than up the side. fn: Fixed fragment count for curved surfaces. Omitted, the ambient ``use_defaults(fn=...)`` value applies; ``fn=0`` opts back out to fa/fs. fa: Minimum fragment angle in degrees. Omitted, the ambient ``use_defaults(fa=...)`` value applies. fs: Minimum fragment size in millimetres. Omitted, the ambient ``use_defaults(fs=...)`` value applies. teardrop: Make the top a printable teardrop, optionally giving the angle. clip_angle: Angle at which a teardrop profile is clipped flat, for printability. """ from pybosl2._helpers import arc_points as _arc_points eff_clip = float(clip_angle) if teardrop is not False and teardrop is not None: # `bool` is a subclass of `int`, so an `isinstance(teardrop, (int, float))` test answers # True for `teardrop=True` and takes the boolean itself as the angle -- a **1 degree** # teardrop, which is a rounding with an imperceptible flat on it rather than the 45 the # flag is supposed to mean. The bool has to be ruled out before the number is read. td_ang = 45.0 if isinstance(teardrop, bool) else float(teardrop) eff_clip = min(eff_clip, 90.0 - td_ang) _check_rim_treatments(radius1, radius2, rounding1, rounding2, chamfer1, chamfer2) path = [[0.0, -length / 2]] if rounding1: sides = max(3, _frag_count(rounding1, fn, fa, fs) // 4) center = [radius1 - rounding1, -length / 2 + rounding1] pts = _arc_points(sides, rounding1, 360 - eff_clip, eff_clip, center) if eff_clip < 90.0: path.append([pts[0][0], -length / 2]) path.extend(pts) elif chamfer1: angle1 = chamfer_angle1 if chamfer_angle1 is not None else 45.0 if from_end1: dx = chamfer1 * math.cos(math.radians(angle1)) dy = chamfer1 * math.sin(math.radians(angle1)) else: dx = chamfer1 dy = chamfer1 * math.tan(math.radians(angle1)) path.append([radius1 - dx, -length / 2]) path.append([radius1, -length / 2 + dy]) else: path.append([radius1, -length / 2]) if rounding2: sides = max(3, _frag_count(rounding2, fn, fa, fs) // 4) center = [radius2 - rounding2, length / 2 - rounding2] pts = _arc_points(sides, rounding2, 0, eff_clip, center) path.extend(pts) if eff_clip < 90.0: path.append([pts[-1][0], length / 2]) elif chamfer2: angle2 = chamfer_angle2 if chamfer_angle2 is not None else 45.0 if from_end2: dx = chamfer2 * math.cos(math.radians(angle2)) dy = chamfer2 * math.sin(math.radians(angle2)) else: dx = chamfer2 dy = chamfer2 * math.tan(math.radians(angle2)) path.append([radius2, length / 2 - dy]) path.append([radius2 - dx, length / 2]) else: path.append([radius2, length / 2]) path.append([0.0, length / 2]) return path
[docs] @backend_only("csg", neutral="pybosl2.solid.xcyl") def xcyl( height: float | None = None, radius: float | None = None, center: bool | None = None, length: float | None = None, radius1: float | None = None, radius2: float | None = None, diameter: float | None = None, diameter1: float | None = None, diameter2: float | None = None, chamfer: float | None = None, chamfer1: float | None = None, chamfer2: float | None = None, rounding: float | None = None, rounding1: float | None = None, rounding2: float | None = None, circumscribe: bool = False, realign: bool = False, shift: Sequence[float] = (0, 0), anchor: Anchor | Sequence[float] | None = None, spin: float = 0, orient: Anchor | Sequence[float] = Anchor.TOP, fn: int | None = None, fa: float | None = None, fs: float | None = None, # Additional missing args chamfer_angle: float | None = None, chamfer_angle1: float | None = None, chamfer_angle2: float | None = None, from_end: bool = False, from_end1: bool | None = None, from_end2: bool | None = None, extra: float = 0.0, extra1: float | None = None, extra2: float | None = None, teardrop: float | bool = False, clip_angle: float = 90.0, texture: str | TextureType | None = None, tex_size: float | Sequence[float] | None = None, tex_reps: int | Sequence[int] | None = None, tex_depth: float = 1.0, tex_inset: float | bool = False, ) -> Bosl2Solid: """Return a cylinder oriented along the X axis. See cyl() for argument details. Args: height: Length of the cylinder along its axis (default 1) radius: Radius of the cylinder (default 1) center: If given, overrides anchor (True -> CENTER, False -> BOTTOM) length: Length of the cylinder along its axis (default 1) radius1: Radius of the negative end of the cylinder. radius2: Radius of the positive end of the cylinder. diameter: Diameter of the cylinder. diameter1: Diameter of the negative end of the cylinder. diameter2: Diameter of the positive end of the cylinder. chamfer: Chamfer size on the end rims (overall/negative/positive) chamfer1: Chamfer size on the end rims (overall/negative/positive) chamfer2: Chamfer size on the end rims (overall/negative/positive) rounding: Rounding radius on the end rims (overall/negative/positive) rounding1: Rounding radius on the end rims (overall/negative/positive) rounding2: Rounding radius on the end rims (overall/negative/positive) circumscribe: Circumscribe rather than inscribe the given radius (CSG backend). realign: Shift point alignment by half a facet (CSG backend). shift: ``[x, y]`` offset for the positive end, shearing the solid (CSG backend). anchor: Anchor point (default CENTER) spin: Z-axis rotation in degrees after anchor (default 0) orient: Direction to rotate the top towards, after spin (default UP) fn: Fixed fragment count for curved surfaces; the ambient default applies when omitted, and 0 means "use fa/fs" (CSG backend). Omitted, the ambient ``use_defaults(fn=...)`` value applies; ``fn=0`` opts back out to fa/fs. fa: Minimum fragment angle in degrees; ambient default when omitted (CSG backend). Omitted, the ambient ``use_defaults(fa=...)`` value applies. fs: Minimum fragment size in millimetres; ambient default when omitted (CSG backend). Omitted, the ambient ``use_defaults(fs=...)`` value applies. chamfer_angle: End chamfer angle in degrees away from the ends (CSG backend). chamfer_angle1: Chamfer angle at the bottom end (CSG backend). chamfer_angle2: Chamfer angle at the top end (CSG backend). from_end: Measure the chamfer along the conic face rather than the axis (CSG backend). from_end1: Measure the bottom chamfer along the conic face (CSG backend). from_end2: Measure the top chamfer along the conic face (CSG backend). extra: Extra height at both ends, invisible to anchoring (CSG backend). extra1: Extra height at the bottom end (CSG backend). extra2: Extra height at the top end (CSG backend). teardrop: Limit the rounding angle from horizontal, for printability (CSG backend). clip_angle: Clip the rounding arc at the bottom of the cylinder (CSG backend). texture: Named texture for the side surface (CSG backend). tex_size: Size of one texture tile (CSG backend). tex_reps: Number of texture repetitions (CSG backend). tex_depth: Depth of the texture (CSG backend). tex_inset: Inset the texture into the surface (CSG backend). Examples: .. pythonscad-example:: from pybosl2.shapes3d.cylinder import xcyl shape = xcyl(radius=10, height=30) shape.show() """ length_val = next((v for v in (length, height) if v is not None), 1.0) rad1 = _pick_radius(radius1=radius1, diameter1=diameter1, radius=radius, diameter=diameter, dflt=1) rad2 = _pick_radius(radius2=radius2, diameter2=diameter2, radius=radius, diameter=diameter, dflt=1) if circumscribe: sides = _frag_count(max(rad1, rad2), fn, fa, fs) sc = 1 / math.cos(math.pi / sides) rad1 *= sc rad2 *= sc use_anchor = _cyl_anchor(center, anchor) shape = cyl( length=length_val, radius1=rad1, radius2=rad2, chamfer=chamfer, chamfer1=chamfer1, chamfer2=chamfer2, rounding=rounding, rounding1=rounding1, rounding2=rounding2, circumscribe=circumscribe, realign=realign, shift=shift, anchor=CENTER, fn=fn, fa=fa, fs=fs, chamfer_angle=chamfer_angle, chamfer_angle1=chamfer_angle1, chamfer_angle2=chamfer_angle2, from_end=from_end, from_end1=from_end1, from_end2=from_end2, extra=extra, extra1=extra1, extra2=extra2, teardrop=teardrop, clip_angle=clip_angle, texture=texture, tex_size=tex_size, tex_reps=tex_reps, tex_depth=tex_depth, tex_inset=tex_inset, ).shape.rotate(90, [0, 1, 0]) offset = _anchor_offset_cyl(rad1, rad2, length_val, use_anchor, axis=0) return _finish3(shape, offset, spin, orient, size=None, anchor=use_anchor)
[docs] @backend_only("csg", neutral="pybosl2.solid.ycyl") def ycyl( height: float | None = None, radius: float | None = None, center: bool | None = None, length: float | None = None, radius1: float | None = None, radius2: float | None = None, diameter: float | None = None, diameter1: float | None = None, diameter2: float | None = None, chamfer: float | None = None, chamfer1: float | None = None, chamfer2: float | None = None, rounding: float | None = None, rounding1: float | None = None, rounding2: float | None = None, circumscribe: bool = False, realign: bool = False, shift: Sequence[float] = (0, 0), anchor: Anchor | Sequence[float] | None = None, spin: float = 0, orient: Anchor | Sequence[float] = Anchor.TOP, fn: int | None = None, fa: float | None = None, fs: float | None = None, # Additional missing args chamfer_angle: float | None = None, chamfer_angle1: float | None = None, chamfer_angle2: float | None = None, from_end: bool = False, from_end1: bool | None = None, from_end2: bool | None = None, extra: float = 0.0, extra1: float | None = None, extra2: float | None = None, teardrop: float | bool = False, clip_angle: float = 90.0, texture: str | TextureType | None = None, tex_size: float | Sequence[float] | None = None, tex_reps: int | Sequence[int] | None = None, tex_depth: float = 1.0, tex_inset: float | bool = False, ) -> Bosl2Solid: """Return a cylinder oriented along the Y axis. See cyl() for argument details. Args: height: Length of the cylinder along its axis (default 1) radius: Radius of the cylinder (default 1) center: If given, overrides anchor (True -> CENTER, False -> BOTTOM) length: Length of the cylinder along its axis (default 1) radius1: Radius of the negative end of the cylinder. radius2: Radius of the positive end of the cylinder. diameter: Diameter of the cylinder. diameter1: Diameter of the negative end of the cylinder. diameter2: Diameter of the positive end of the cylinder. chamfer: Chamfer size on the end rims (overall/negative/positive) chamfer1: Chamfer size on the end rims (overall/negative/positive) chamfer2: Chamfer size on the end rims (overall/negative/positive) rounding: Rounding radius on the end rims (overall/negative/positive) rounding1: Rounding radius on the end rims (overall/negative/positive) rounding2: Rounding radius on the end rims (overall/negative/positive) circumscribe: Circumscribe rather than inscribe the given radius (CSG backend). realign: Shift point alignment by half a facet (CSG backend). shift: ``[x, y]`` offset for the positive end, shearing the solid (CSG backend). anchor: Anchor point (default CENTER) spin: Z-axis rotation in degrees after anchor (default 0) orient: Direction to rotate the top towards, after spin (default UP) fn: Fixed fragment count for curved surfaces; the ambient default applies when omitted, and 0 means "use fa/fs" (CSG backend). Omitted, the ambient ``use_defaults(fn=...)`` value applies; ``fn=0`` opts back out to fa/fs. fa: Minimum fragment angle in degrees; ambient default when omitted (CSG backend). Omitted, the ambient ``use_defaults(fa=...)`` value applies. fs: Minimum fragment size in millimetres; ambient default when omitted (CSG backend). Omitted, the ambient ``use_defaults(fs=...)`` value applies. chamfer_angle: End chamfer angle in degrees away from the ends (CSG backend). chamfer_angle1: Chamfer angle at the bottom end (CSG backend). chamfer_angle2: Chamfer angle at the top end (CSG backend). from_end: Measure the chamfer along the conic face rather than the axis (CSG backend). from_end1: Measure the bottom chamfer along the conic face (CSG backend). from_end2: Measure the top chamfer along the conic face (CSG backend). extra: Extra height at both ends, invisible to anchoring (CSG backend). extra1: Extra height at the bottom end (CSG backend). extra2: Extra height at the top end (CSG backend). teardrop: Limit the rounding angle from horizontal, for printability (CSG backend). clip_angle: Clip the rounding arc at the bottom of the cylinder (CSG backend). texture: Named texture for the side surface (CSG backend). tex_size: Size of one texture tile (CSG backend). tex_reps: Number of texture repetitions (CSG backend). tex_depth: Depth of the texture (CSG backend). tex_inset: Inset the texture into the surface (CSG backend). Examples: .. pythonscad-example:: from pybosl2.shapes3d.cylinder import ycyl shape = ycyl(radius=10, height=30) shape.show() """ length_val = next((v for v in (length, height) if v is not None), 1.0) rad1 = _pick_radius(radius1=radius1, diameter1=diameter1, radius=radius, diameter=diameter, dflt=1) rad2 = _pick_radius(radius2=radius2, diameter2=diameter2, radius=radius, diameter=diameter, dflt=1) if circumscribe: sides = _frag_count(max(rad1, rad2), fn, fa, fs) sc = 1 / math.cos(math.pi / sides) rad1 *= sc rad2 *= sc use_anchor = _cyl_anchor(center, anchor) shape = cyl( length=length_val, radius1=rad1, radius2=rad2, chamfer=chamfer, chamfer1=chamfer1, chamfer2=chamfer2, rounding=rounding, rounding1=rounding1, rounding2=rounding2, circumscribe=circumscribe, realign=realign, shift=shift, anchor=CENTER, fn=fn, fa=fa, fs=fs, chamfer_angle=chamfer_angle, chamfer_angle1=chamfer_angle1, chamfer_angle2=chamfer_angle2, from_end=from_end, from_end1=from_end1, from_end2=from_end2, extra=extra, extra1=extra1, extra2=extra2, teardrop=teardrop, clip_angle=clip_angle, texture=texture, tex_size=tex_size, tex_reps=tex_reps, tex_depth=tex_depth, tex_inset=tex_inset, ).shape.rotate(-90, [1, 0, 0]) offset = _anchor_offset_cyl(rad1, rad2, length_val, use_anchor, axis=1) return _finish3(shape, offset, spin, orient, size=None, anchor=use_anchor)
[docs] @backend_only("csg", neutral="pybosl2.solid.zcyl") def zcyl( height: float | None = None, radius: float | None = None, center: bool | None = None, length: float | None = None, radius1: float | None = None, radius2: float | None = None, diameter: float | None = None, diameter1: float | None = None, diameter2: float | None = None, chamfer: float | None = None, chamfer1: float | None = None, chamfer2: float | None = None, rounding: float | None = None, rounding1: float | None = None, rounding2: float | None = None, circumscribe: bool = False, realign: bool = False, shift: Sequence[float] = (0, 0), anchor: Anchor | Sequence[float] | None = None, spin: float = 0, orient: Anchor | Sequence[float] = Anchor.TOP, fn: int | None = None, fa: float | None = None, fs: float | None = None, # Additional missing args chamfer_angle: float | None = None, chamfer_angle1: float | None = None, chamfer_angle2: float | None = None, from_end: bool = False, from_end1: bool | None = None, from_end2: bool | None = None, extra: float = 0.0, extra1: float | None = None, extra2: float | None = None, teardrop: float | bool = False, clip_angle: float = 90.0, texture: str | TextureType | None = None, tex_size: float | Sequence[float] | None = None, tex_reps: int | Sequence[int] | None = None, tex_depth: float = 1.0, tex_inset: float | bool = False, ) -> Bosl2Solid: """Return a cylinder oriented along the Z axis (same as cyl() with default orientation). See cyl() for. argument details. Args: height: Length of the cylinder along its axis (default 1) radius: Radius of the cylinder (default 1) center: If given, overrides anchor (True -> CENTER, False -> BOTTOM) length: Length of the cylinder along its axis (default 1) radius1: Radius of the negative end of the cylinder. radius2: Radius of the positive end of the cylinder. diameter: Diameter of the cylinder. diameter1: Diameter of the negative end of the cylinder. diameter2: Diameter of the positive end of the cylinder. chamfer: Chamfer size on the end rims (overall/negative/positive) chamfer1: Chamfer size on the end rims (overall/negative/positive) chamfer2: Chamfer size on the end rims (overall/negative/positive) rounding: Rounding radius on the end rims (overall/negative/positive) rounding1: Rounding radius on the end rims (overall/negative/positive) rounding2: Rounding radius on the end rims (overall/negative/positive) circumscribe: Circumscribe rather than inscribe the given radius (CSG backend). realign: Shift point alignment by half a facet (CSG backend). shift: ``[x, y]`` offset for the positive end, shearing the solid (CSG backend). anchor: Anchor point (default CENTER) spin: Z-axis rotation in degrees after anchor (default 0) orient: Direction to rotate the top towards, after spin (default UP) fn: Fixed fragment count for curved surfaces; the ambient default applies when omitted, and 0 means "use fa/fs" (CSG backend). Omitted, the ambient ``use_defaults(fn=...)`` value applies; ``fn=0`` opts back out to fa/fs. fa: Minimum fragment angle in degrees; ambient default when omitted (CSG backend). Omitted, the ambient ``use_defaults(fa=...)`` value applies. fs: Minimum fragment size in millimetres; ambient default when omitted (CSG backend). Omitted, the ambient ``use_defaults(fs=...)`` value applies. chamfer_angle: End chamfer angle in degrees away from the ends (CSG backend). chamfer_angle1: Chamfer angle at the bottom end (CSG backend). chamfer_angle2: Chamfer angle at the top end (CSG backend). from_end: Measure the chamfer along the conic face rather than the axis (CSG backend). from_end1: Measure the bottom chamfer along the conic face (CSG backend). from_end2: Measure the top chamfer along the conic face (CSG backend). extra: Extra height at both ends, invisible to anchoring (CSG backend). extra1: Extra height at the bottom end (CSG backend). extra2: Extra height at the top end (CSG backend). teardrop: Limit the rounding angle from horizontal, for printability (CSG backend). clip_angle: Clip the rounding arc at the bottom of the cylinder (CSG backend). texture: Named texture for the side surface (CSG backend). tex_size: Size of one texture tile (CSG backend). tex_reps: Number of texture repetitions (CSG backend). tex_depth: Depth of the texture (CSG backend). tex_inset: Inset the texture into the surface (CSG backend). Examples: .. pythonscad-example:: from pybosl2.shapes3d.cylinder import zcyl shape = zcyl(radius=10, height=30) shape.show() """ return cyl( height=height, radius=radius, center=center, length=length, radius1=radius1, radius2=radius2, diameter=diameter, diameter1=diameter1, diameter2=diameter2, chamfer=chamfer, chamfer1=chamfer1, chamfer2=chamfer2, rounding=rounding, rounding1=rounding1, rounding2=rounding2, circumscribe=circumscribe, realign=realign, shift=shift, anchor=anchor, spin=spin, orient=orient, fn=fn, fa=fa, fs=fs, chamfer_angle=chamfer_angle, chamfer_angle1=chamfer_angle1, chamfer_angle2=chamfer_angle2, from_end=from_end, from_end1=from_end1, from_end2=from_end2, extra=extra, extra1=extra1, extra2=extra2, teardrop=teardrop, clip_angle=clip_angle, texture=texture, tex_size=tex_size, tex_reps=tex_reps, tex_depth=tex_depth, tex_inset=tex_inset, )
[docs] @backend_only("csg", neutral="pybosl2.solid.tube") def tube( height: float | None = None, outer_radius: float | None = None, inner_radius: float | None = None, center: bool | None = None, outer_diameter: float | None = None, inner_diameter: float | None = None, wall: float | None = None, outer_radius1: float | None = None, outer_radius2: float | None = None, outer_diameter1: float | None = None, outer_diameter2: float | None = None, inner_radius1: float | None = None, inner_radius2: float | None = None, inner_diameter1: float | None = None, inner_diameter2: float | None = None, chamfer: float | None = None, chamfer1: float | None = None, chamfer2: float | None = None, rounding: float | None = None, rounding1: float | None = None, rounding2: float | None = None, realign: bool = False, length: float | None = None, anchor: Anchor | Sequence[float] = Anchor.CENTER, spin: float = 0, orient: Anchor | Sequence[float] = Anchor.TOP, fn: int | None = None, fa: float | None = None, fs: float | None = None, ) -> Bosl2Solid: """Return a hollow cylindrical tube, with optional chamfer/rounding on end rims. Note: BOSL2's outer-radius parameters are named ``or``/``or1``/``or2``, which collide with the Python keyword ``or``; they are exposed here as ``outer_radius``/``outer_radius1``/``outer_radius2`` instead. Args: height: height of the tube (default 1) length: height of the tube (default 1) outer_radius: outer radius of the tube (BOSL2 ``or``) (default 1) inner_radius: inner radius of the tube center: if given, overrides anchor (True -> CENTER, False -> DOWN) outer_diameter: outer diameter of the tube inner_diameter: inner diameter of the tube wall: horizontal wall thickness (default 1) outer_radius1: outer radius of the bottom/top outer_radius2: outer radius of the bottom/top outer_diameter1: outer diameter of the bottom/top outer_diameter2: outer diameter of the bottom/top inner_radius1: inner radius of the bottom/top inner_radius2: inner radius of the bottom/top inner_diameter1: inner diameter of the bottom/top inner_diameter2: inner diameter of the bottom/top chamfer: chamfer size on end rims (overall/bottom/top) chamfer1: chamfer size on end rims (overall/bottom/top) chamfer2: chamfer size on end rims (overall/bottom/top) rounding: rounding radius on end rims (overall/bottom/top) rounding1: rounding radius on end rims (overall/bottom/top) rounding2: rounding radius on end rims (overall/bottom/top) realign: rotate by half the angle of one face (default False) anchor: anchor point (default CENTER) spin: Z-axis rotation in degrees after anchor (default 0) orient: direction to rotate the top towards, after spin (default UP) fn: arc smoothness overrides. Omitted, the ambient ``use_defaults(fn=...)`` value applies; ``fn=0`` opts back out to fa/fs. fa: arc smoothness overrides. Omitted, the ambient ``use_defaults(fa=...)`` value applies. fs: arc smoothness overrides. Omitted, the ambient ``use_defaults(fs=...)`` value applies. Examples: .. pythonscad-example:: from pybosl2 import tube shape = tube(height=20, outer_radius=15, inner_radius=10) shape.show() A tube with chamfered end rims: .. pythonscad-example:: from pybosl2 import tube shape = tube(height=20, outer_radius=15, inner_radius=10, chamfer=1) shape.show() """ height = height if height is not None else (length if length is not None else 1) orr1 = _pick_radius( radius1=outer_radius1, diameter1=outer_diameter1, radius=outer_radius, diameter=outer_diameter, dflt=None, ) orr2 = _pick_radius( radius1=outer_radius2, diameter1=outer_diameter2, radius=outer_radius, diameter=outer_diameter, dflt=None, ) irr1 = _pick_radius( radius1=inner_radius1, diameter1=inner_diameter1, radius=inner_radius, diameter=inner_diameter, dflt=None, ) irr2 = _pick_radius( radius1=inner_radius2, diameter1=inner_diameter2, radius=inner_radius, diameter=inner_diameter, dflt=None, ) wall_v = wall if wall is not None else 1 rad1 = orr1 if orr1 is not None else (irr1 + wall_v if irr1 is not None else None) rad2 = orr2 if orr2 is not None else (irr2 + wall_v if irr2 is not None else None) irad1 = irr1 if irr1 is not None else (orr1 - wall_v if orr1 is not None else None) irad2 = irr2 if irr2 is not None else (orr2 - wall_v if orr2 is not None else None) if rad1 is None or rad2 is None or irad1 is None or irad2 is None: raise Bosl2ValueError( "tube(): needs two of the three sizes -- an inner radius/diameter, an outer " "radius/diameter, and a wall thickness." ) if not (irad1 <= rad1): raise Bosl2ValueError("tube(): inner radius is larger than outer radius.") if not (irad2 <= rad2): raise Bosl2ValueError("tube(): inner radius is larger than outer radius.") use_anchor = resolve_center_anchor(center=center, anchor=anchor, centred=Anchor.CENTER, uncentred=BOTTOM) # Build outer and inner cylinders via cyl() for chamfer/rounding support outer = cyl( height=height, radius1=rad1, radius2=rad2, center=True, chamfer=chamfer, chamfer1=chamfer1, chamfer2=chamfer2, rounding=rounding, rounding1=rounding1, rounding2=rounding2, fn=fn, fa=fa, fs=fs, ) extra_h = max(chamfer or 0, rounding or 0, chamfer1 or 0, rounding1 or 0, chamfer2 or 0, rounding2 or 0) inner = cyl( height=height + extra_h + 0.02, radius1=irad1, radius2=irad2, center=True, fn=fn, fa=fa, fs=fs, ) shape = outer.shape - inner.shape if realign: sides = _frag_count(max(rad1, rad2), fn, fa, fs) shape = shape.rotate(180 / sides, [0, 0, 1]) offset = _anchor_offset_cyl(rad1, rad2, height, use_anchor) return _finish3(shape, offset, spin, orient, size=None, anchor=use_anchor)
[docs] @backend_only("csg") def cone( height: float | None = None, radius: float | None = None, radius1: float | None = None, radius2: float | None = None, center: bool | None = None, diameter: float | None = None, diameter1: float | None = None, diameter2: float | None = None, chamfer: float | None = None, chamfer1: float | None = None, chamfer2: float | None = None, rounding: float | None = None, rounding1: float | None = None, rounding2: float | None = None, length: float | None = None, anchor: Anchor | Sequence[float] | None = None, spin: float = 0, orient: Anchor | Sequence[float] = Anchor.TOP, fn: int | None = None, fa: float | None = None, fs: float | None = None, ) -> Bosl2Solid: """Return a cone/truncated cone with optional chamfering or rounding of the end rims. Convenience wrapper around :func:`cyl` / :func:`cylinder` with ``radius2=0`` by default for a pointed cone, or with explicit ``radius2`` for a truncated (frustum) form. Args: height: height of the cone (default 1) length: height of the cone (default 1) radius: base radius (default 1) radius1: bottom radius (overrides *radius*) radius2: top radius (default 0 for a pointed cone) center: if given, overrides anchor diameter: base diameter diameter1: bottom diameter diameter2: top diameter chamfer: chamfer size on the end rims chamfer1: chamfer size on the end rims chamfer2: chamfer size on the end rims rounding: rounding radius on the end rims rounding1: rounding radius on the end rims rounding2: rounding radius on the end rims anchor: anchor point spin: Z-axis rotation in degrees after anchor (default 0) orient: direction to rotate the top towards, after spin (default UP) fn: arc smoothness overrides. Omitted, the ambient ``use_defaults(fn=...)`` value applies; ``fn=0`` opts back out to fa/fs. fa: arc smoothness overrides. Omitted, the ambient ``use_defaults(fa=...)`` value applies. fs: arc smoothness overrides. Omitted, the ambient ``use_defaults(fs=...)`` value applies. Examples: A pointed cone: .. pythonscad-example:: from pybosl2 import shapes3d as s3 s3.cone(height=30, radius=15).show() A truncated cone (frustum): .. pythonscad-example:: from pybosl2 import shapes3d as s3 s3.cone(height=30, radius1=15, radius2=8).show() A cone with chamfered base: .. pythonscad-example:: from pybosl2 import shapes3d as s3 s3.cone(height=30, radius1=15, radius2=3, chamfer=2).show() """ r1 = _pick_radius(radius1=radius1, diameter1=diameter1, radius=radius, diameter=diameter, dflt=1) r2 = _pick_radius(radius2=radius2, diameter2=diameter2, dflt=0) return cyl( height=height, radius1=r1, radius2=r2, center=center, length=length, chamfer=chamfer, chamfer1=chamfer1, chamfer2=chamfer2, rounding=rounding, rounding1=rounding1, rounding2=rounding2, anchor=anchor, spin=spin, orient=orient, fn=fn, fa=fa, fs=fs, )