# 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/shapes2d/circle.py
# FileSummary: Circles, ellipses, arcs, keyholes and rings.
# DocCategory: Foundational
# FileGroup: BOSL2
from __future__ import annotations
import math
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Union
import numpy as np
# Import base class and helper functions from shapes2d.base
from pybosl2._helpers import (
anchor_offset_box as _anchor_offset_box,
)
from pybosl2._helpers import (
anchor_offset_hull as _anchor_offset_hull,
)
from pybosl2._helpers import (
arc_points as _arc_points,
)
from pybosl2._helpers import (
circle_from_3pts as _circle_from_3pts,
)
from pybosl2._helpers import (
circle_pts as _circle_pts,
)
from pybosl2._helpers import (
frag_count as _frag_count,
)
from pybosl2._helpers import (
pick_radius as _pick_radius,
)
from pybosl2._helpers import (
polar_to_xy as _polar_to_xy,
)
from pybosl2._native import native
from pybosl2.constants import CENTER
from pybosl2.geometry import is_collinear
from pybosl2.path2d import Path2D
from pybosl2.points import Point
from pybosl2.vectors import unit
from .base import (
Bosl2Shape2D,
_circle_from_corner,
_det2,
_finish,
_sign,
_vector_angle,
)
if TYPE_CHECKING:
from openscad import PyOpenSCAD
from pybosl2._edges_lang import Anchor
Shape2DLike = Union["Bosl2Shape2D", "PyOpenSCAD", "Path2D", Sequence[Sequence[float]], np.ndarray]
if TYPE_CHECKING: # real stub-typed imports for the checker (identical to pre-lazy)
from pythonscad import circle as _ocircle
from pythonscad import fill as _ofill
from pythonscad import hull as _ohull
from pythonscad import polygon as _opolygon
from pythonscad import square as _osquare
from pythonscad import text as _otext
else:
_ocircle = native("circle")
_ofill = native("fill")
_ohull = native("hull")
_opolygon = native("polygon")
_osquare = native("square")
_otext = native("text")
[docs]
def circle(
radius: float | None = None,
diameter: float | None = None,
points: Sequence[Sequence[float]] | None = None,
corner: Sequence[Sequence[float]] | None = None,
anchor: Anchor | Sequence[float] = CENTER,
spin: float = 0,
fn: int | None = None,
fa: float | None = None,
fs: float | None = None,
) -> Bosl2Shape2D:
"""A circle, built with the builtin circle(), by radius/diameter, or fit to points.
If `corner` is given three 2-D points, the circle is centered to be tangent to both
segments of that path, on the inside corner. If `points` is given three 2-D points,
the circle is centered and sized to pass through all three points. Anchor/spin are
ignored for the `corner`/`points` forms, matching BOSL2.
Args:
radius: radius of the circle
diameter: diameter of the circle
points: three 2-D points the circle should pass through
corner: three 2-D points defining a path the circle should be tangent to
anchor: anchor point (default CENTER)
spin: Z-axis rotation in degrees after anchor (default 0)
fn: arc smoothness overrides
fa: arc smoothness overrides
fs: arc smoothness overrides
Examples:
.. pythonscad-example::
from pybosl2 import shapes2d as s2
s2.circle(radius=15).linear_extrude(height=5).show()
"""
if points is not None:
center, rad = _circle_from_3pts(points)
return _finish(_ocircle(r=rad, fn=fn, fa=fa, fs=fs), center, 0, size=[2 * rad, 2 * rad])
if corner is not None:
rad = radius if radius is not None else (diameter / 2 if diameter is not None else 1)
center = _circle_from_corner(corner, rad)
return _finish(_ocircle(r=rad, fn=fn, fa=fa, fs=fs), center, 0, size=[2 * rad, 2 * rad])
rad = radius if radius is not None else (diameter / 2 if diameter is not None else 1)
shape = _ocircle(r=rad, fn=fn, fa=fa, fs=fs)
n = _frag_count(rad, fn, fa, fs)
offset = _anchor_offset_hull(_circle_pts(rad, n), anchor)
return _finish(shape, offset, spin, size=[2 * rad, 2 * rad], anchor=anchor)
[docs]
def arc(
count: int | None = None,
radius: float | None = None,
angle: float | Sequence[float] | None = None,
diameter: float | None = None,
center: Sequence[float] | None = None,
points: Sequence[Sequence[float]] | None = None,
corner: Sequence[Sequence[float]] | None = None,
width: float | None = None,
thickness: float | None = None,
start: float | None = None,
wedge: bool = False,
long: bool = False,
clockwise: bool = False,
counterclockwise: bool = False,
endpoint: bool = True,
fn: int | None = None,
fa: float | None = None,
fs: float | None = None,
) -> Path2D:
"""A 2-D arc, returned as a :class:`~pybosl2.paths.Path2D` of points (BOSL2's ``arc()``).
All of BOSL2's 2-D arc specifications are supported (3-D arcs, which project onto a plane,
are not):
* ``arc(radius=, angle=, [start=], [center=])`` -- radius about *center*, sweeping *angle* degrees from
*start* (or ``angle=[start, end]`` for an explicit range).
* ``arc(width=, thickness=)`` -- a circular segment starting and ending on the X axis.
* ``arc(center=, points=[P0, P1])`` -- around *center* from ``P0`` toward the direction of ``P1``; the
short way by default, or the long/``clockwise``/``counterclockwise`` way.
* ``arc(points=[P0, P1, P2])`` -- through three points, from ``P0`` via ``P1`` to ``P2``.
* ``arc(corner=[P0, P1, P2], radius=)`` -- the fillet arc of radius tangent to both legs of the
corner ``P0-P1-P2``.
Set ``wedge=True`` to prepend the centre point, giving a closed pie/sector path. When *count* is
omitted the point count follows OpenSCAD's $fn/$fa/$fs rules, matching BOSL2.
Args:
count: number of points (default: from $fn/$fa/$fs)
radius: radius of the arc
diameter: diameter of the arc
angle: degrees to sweep from *start*, or ``[start, end]``
center: centre point (default ``[0, 0]``)
points: two points (with *center*) or three points the arc passes through
corner: three points; the arc is the radius fillet tangent to both legs
width: chord width for the width/thickness form
thickness: height of the circular segment for the width/thickness form
start: starting angle in degrees (default 0)
wedge: prepend the centre point, producing a closed sector (default False)
long: for the two-point form, take the long way / a given handedness
clockwise: for the two-point form, take the long way / a given handedness
counterclockwise: for the two-point form, take the long way / a given handedness
endpoint: include the final point (default True)
Returns:
A :class:`~pybosl2.paths.Path2D` (closed when *wedge* is set).
"""
# -- width + thickness: a circular segment through 3 points on/above the X axis ----------
if width is not None and thickness is not None:
assert not any(v is not None for v in (radius, center, points, angle, start)), "conflicting arc() params"
return arc(
count=count,
points=[[width / 2, 0], [0, thickness], [-width / 2, 0]],
wedge=wedge,
endpoint=endpoint,
fn=fn,
fa=fa,
fs=fs,
)
# -- corner: the fillet arc tangent to both legs of a 3-point corner ---------------------
if corner is not None:
assert len(corner) == 3, "corner= needs exactly 3 points"
assert not is_collinear(
Point(corner[0][0], corner[0][1]), Point(corner[1][0], corner[1][1]), Point(corner[2][0], corner[2][1])
), "Collinear corner does not define an arc"
rad = _pick_radius(radius=radius, diameter=diameter)
assert rad is not None and rad > 0, "arc(corner=) needs radius= or diameter="
p0, p1, p2 = corner
v1 = unit([float(p0[0]) - float(p1[0]), float(p0[1]) - float(p1[1])])
v2 = unit([float(p2[0]) - float(p1[0]), float(p2[1]) - float(p1[1])])
half = math.acos(max(-1.0, min(1.0, v1[0] * v2[0] + v1[1] * v2[1]))) / 2
d_tan = rad / math.tan(half)
cp2 = _circle_from_corner(corner, rad)
tp1 = [float(p1[0]) + v1[0] * d_tan, float(p1[1]) + v1[1] * d_tan]
tp2 = [float(p1[0]) + v2[0] * d_tan, float(p1[1]) + v2[1] * d_tan]
forward = (
_det2(
[float(p1[0]) - float(p0[0]), float(p1[1]) - float(p0[1])],
[float(p2[0]) - float(p1[0]), float(p2[1]) - float(p1[1])],
)
> 0
)
c0, c1 = (tp1, tp2) if forward else (tp2, tp1)
ts = math.degrees(math.atan2(c0[1] - cp2[1], c0[0] - cp2[0]))
te = math.degrees(math.atan2(c1[1] - cp2[1], c1[0] - cp2[0]))
sweep = (te - ts) % 360
rng = [ts, ts + sweep] if forward else [ts + sweep, ts]
return arc(
count=count,
center=cp2,
radius=rad,
angle=rng,
wedge=wedge,
endpoint=endpoint,
fn=fn,
fa=fa,
fs=fs,
)
# -- points forms ------------------------------------------------------------------------
if points is not None:
pts = [[float(p[0]), float(p[1])] for p in points]
assert all(len(p) == 2 for p in points), "arc() port handles 2-D points only"
if len(pts) == 2:
assert center is not None, "center= is required when points has length 2"
assert pts[0] != pts[1], "arc endpoints are equal"
centre = [float(center[0]), float(center[1])]
dv1 = [float(pts[0][0]) - centre[0], float(pts[0][1]) - centre[1]]
dv2 = [float(pts[1][0]) - centre[0], float(pts[1][1]) - centre[1]]
angle_val = _vector_angle(pts[0], centre, pts[1])
prelim = _sign(_det2(dv1, dv2))
if prelim != 0:
direction = prelim
else:
assert clockwise or counterclockwise, "Collinear inputs don't define a unique arc"
direction = 1
rad = math.hypot(dv1[0], dv1[1])
if long or (counterclockwise and direction < 0) or (clockwise and direction > 0):
final_angle = -direction * (360 - angle_val)
else:
final_angle = direction * angle_val
sa = math.degrees(math.atan2(dv1[1], dv1[0]))
return arc(
count=count,
center=centre,
radius=rad,
start=sa,
angle=final_angle,
wedge=wedge,
endpoint=endpoint,
fn=fn,
fa=fa,
fs=fs,
)
assert len(pts) == 3, f"arc(points=) needs 2 or 3 points, got {len(pts)}"
assert not is_collinear(
Point(pts[0][0], pts[0][1]), Point(pts[1][0], pts[1][1]), Point(pts[2][0], pts[2][1])
), "Collinear inputs do not define an arc"
centre, arc_radius = _circle_from_3pts(pts)
a0 = math.degrees(math.atan2(pts[0][1] - centre[1], pts[0][0] - centre[0]))
am = math.degrees(math.atan2(pts[1][1] - centre[1], pts[1][0] - centre[0]))
a1 = math.degrees(math.atan2(pts[2][1] - centre[1], pts[2][0] - centre[0]))
d_mid = (am - a0) % 360
d_end = (a1 - a0) % 360
delta = d_end if d_mid <= d_end else d_end - 360
point_count = (
count if count is not None else max(3, math.ceil(_frag_count(arc_radius, fn, fa, fs) * abs(delta) / 360))
)
out = _arc_points(point_count, arc_radius, a0, delta, centre, endpoint=endpoint)
if wedge:
out = [list(centre)] + out
return Path2D(out, closed=wedge)
# -- radius + angle (with optional [start, end] range) -----------------------------------
arc_r: float | None = _pick_radius(radius=radius, diameter=diameter)
assert arc_r is not None, "arc() needs radius=/diameter=, points=, corner=, or width=/thickness="
if isinstance(angle, (list, tuple)):
assert start is None, "start= is not allowed with angle=[start, end]"
calc_start = float(angle[0])
calc_angle = float(angle[1]) - float(angle[0])
elif isinstance(angle, (int, float)):
calc_angle = float(angle)
calc_start = 0.0 if start is None else float(start)
elif angle is None:
calc_angle = 360.0
calc_start = 0.0 if start is None else float(start)
else:
raise TypeError(f"angle must be a number, a [start, end] pair, or None, got {type(angle)}")
calc_center = (0.0, 0.0) if center is None else center
point_count = count if count is not None else math.ceil(_frag_count(arc_r, fn, fa, fs) * abs(calc_angle) / 360) + 1
out = _arc_points(point_count, arc_r, calc_start, calc_angle, calc_center, endpoint=endpoint)
if wedge:
out = [list(calc_center)] + out
return Path2D(out, closed=wedge)
[docs]
def ellipse(
radius: float | Sequence[float] | None = None,
diameter: float | Sequence[float] | None = None,
realign: bool = False,
circumscribe: bool = False,
uniform: bool = False,
anchor: Anchor | Sequence[float] = CENTER,
spin: float = 0,
fn: int | None = None,
fa: float | None = None,
fs: float | None = None,
) -> Bosl2Shape2D:
"""An ellipse (approximated as a polygon), built directly with polygon().
Note: `uniform` (equal-length approximating segments) is not implemented; segments are
evenly spaced by angle instead.
Args:
radius: radius of the circle, or pair of semi-axes of the ellipse
diameter: diameter of the circle, or pair giving the full X/Y axis lengths
realign: shift the first polygon point off the X+ axis (default False)
circumscribe: circumscribe rather than inscribe the ideal ellipse (default False)
anchor: anchor point (default CENTER)
spin: Z-axis rotation in degrees after anchor (default 0)
fn: arc smoothness overrides
fa: arc smoothness overrides
fs: arc smoothness overrides
Examples:
.. pythonscad-example::
from pybosl2 import shapes2d as s2
s2.ellipse(diameter=[30, 20]).linear_extrude(height=5).show()
"""
_ = uniform
if radius is not None:
rad = [float(radius), float(radius)] if isinstance(radius, (int, float)) else [float(v) for v in radius]
elif diameter is not None:
dd = [float(diameter), float(diameter)] if isinstance(diameter, (int, float)) else [float(v) for v in diameter]
rad = [dd[0] / 2, dd[1] / 2]
else:
rad = [1.0, 1.0]
n = _frag_count(max(rad), fn, fa, fs)
scale = 1.0 / math.cos(math.pi / n) if circumscribe else 1.0
start = (360.0 / n) / 2 if realign else 0.0
path = [
[
rad[0] * scale * math.cos(math.radians(start + 360.0 * i / n)),
rad[1] * scale * math.sin(math.radians(start + 360.0 * i / n)),
]
for i in range(n)
]
shape = _opolygon(path)
offset = _anchor_offset_hull(path, anchor)
return _finish(shape, offset, spin)
[docs]
def keyhole(
length: float | None = None,
radius1: float | None = None,
radius2: float | None = None,
shoulder_radius: float = 0,
diameter1: float | None = None,
diameter2: float | None = None,
_length: float | None = None,
anchor: Anchor | Sequence[float] = CENTER,
spin: float = 0,
fn: int | None = None,
fa: float | None = None,
fs: float | None = None,
) -> Bosl2Shape2D:
"""A keyhole slot -- a small circle joined to a larger one by tangent shoulders (BOSL2 keyhole()).
Args:
length: overall length between the two circle centers (default 15)
radius1: radius/diameter of the small (bottom) circle (default 5)
diameter1: radius/diameter of the small (bottom) circle (default 5)
radius2: radius/diameter of the large (top) circle (default 10)
diameter2: radius/diameter of the large (top) circle (default 10)
shoulder_radius: fillet radius where the shoulders meet the circles (default 0)
anchor: standard BOSL2 2-D anchor / spin
spin: standard BOSL2 2-D anchor / spin
Examples:
.. pythonscad-example::
from pybosl2 import shapes2d as s2
s2.keyhole(length=25, radius1=4, radius2=9, shoulder_radius=2).linear_extrude(height=4).show()
"""
lv = float(length if length is not None else (_length if _length is not None else 15))
r1v = float(radius1 if radius1 is not None else (diameter1 / 2 if diameter1 is not None else 5))
r2v = float(radius2 if radius2 is not None else (diameter2 / 2 if diameter2 is not None else 10))
assert lv > 0 and lv >= max(r1v, r2v), "keyhole(): length must be positive and at least max(radius1, radius2)."
shoulder_radius = float(shoulder_radius) if shoulder_radius is not None else min(r1v, r2v) / 2
cp1, cp2 = [0.0, 0.0], [0.0, -lv]
minr, maxr = min(r1v, r2v) + shoulder_radius, max(r1v, r2v) + shoulder_radius
dy = math.sqrt(maxr * maxr - minr * minr)
spt1 = [cp1[0] + minr, cp1[1] - dy] if r1v > r2v else [cp2[0] + minr, cp2[1] + dy]
spt2 = [-spt1[0], spt1[1]]
base = cp1 if r1v > r2v else cp2
ds = [spt1[0] - base[0], spt1[1] - base[1]]
angle = math.degrees(math.atan2(abs(ds[1]), abs(ds[0])))
def _arc(**kw): # type: ignore[no-untyped-def]
return arc(endpoint=False, fn=fn, fa=fa, fs=fs, **kw)
path: list[Any] = []
if r1v > r2v:
path += (
[spt1]
if shoulder_radius <= 0
else _arc(radius=shoulder_radius, center=spt1, start=180 - angle, angle=angle) # type: ignore[no-untyped-call]
)
path += _arc(radius=r2v, center=cp2, start=0, angle=-180) # type: ignore[no-untyped-call]
path += [spt2] if shoulder_radius <= 0 else _arc(radius=shoulder_radius, center=spt2, start=0, angle=angle) # type: ignore[no-untyped-call]
path += _arc(radius=r1v, center=cp1, start=180 + angle, angle=-180 - 2 * angle) # type: ignore[no-untyped-call]
else:
path += [spt1] if shoulder_radius <= 0 else _arc(radius=shoulder_radius, center=spt1, start=180, angle=angle) # type: ignore[no-untyped-call]
path += _arc(radius=r2v, center=cp2, start=angle, angle=-180 - 2 * angle) # type: ignore[no-untyped-call]
path += (
[spt2]
if shoulder_radius <= 0
else _arc(radius=shoulder_radius, center=spt2, start=360 - angle, angle=angle) # type: ignore[no-untyped-call]
)
path += _arc(radius=r1v, center=cp1, start=180, angle=-180) # type: ignore[no-untyped-call]
shape = _opolygon(path)
offset = _anchor_offset_hull(path, anchor)
return _finish(shape, offset, spin)
[docs]
def ring(
sides: int | None = None,
ring_width: float | None = None,
radius: float | None = None,
radius1: float | None = None,
radius2: float | None = None,
diameter: float | None = None,
diameter1: float | None = None,
diameter2: float | None = None,
angle: float | None = None,
anchor: Anchor | Sequence[float] = CENTER,
spin: float = 0,
fn: int | None = None,
fa: float | None = None,
fs: float | None = None,
) -> Bosl2Shape2D:
"""A 2-D ring (annulus) between two concentric radii (BOSL2 ring(), full-annulus form).
Give either both radii (*radius1*/*radius2* or *diameter1*/*diameter2*) or one radius plus
*ring_width*. The arc / 3-point / corner / width+thickness forms of BOSL2 ``ring()`` are
not ported.
Args:
radius1: the two radii/diameters
radius2: the two radii/diameters
diameter1: the two radii/diameters
diameter2: the two radii/diameters
radius: one radius plus the wall width
diameter: one radius plus the wall width
ring_width: one radius plus the wall width
sides: number of sides (overrides the smoothness overrides)
anchor: standard BOSL2 2-D anchor / spin
spin: standard BOSL2 2-D anchor / spin
Examples:
.. pythonscad-example::
from pybosl2 import shapes2d as s2
s2.ring(radius=20, ring_width=4).linear_extrude(height=5).show()
"""
assert angle is None, "ring(): only the full-annulus form is ported (no angle=)."
r1v = radius1 if radius1 is not None else (diameter1 / 2 if diameter1 is not None else None)
r2v = radius2 if radius2 is not None else (diameter2 / 2 if diameter2 is not None else None)
rv = radius if radius is not None else (diameter / 2 if diameter is not None else None)
if r1v is not None and r2v is not None:
inner, outer = min(r1v, r2v), max(r1v, r2v)
else:
assert rv is not None and ring_width is not None, (
"ring(): give (radius1 and radius2) or (radius and ring_width)."
)
inner, outer = min(rv, rv + ring_width), max(rv, rv + ring_width)
assert inner != outer and outer > 0, "ring(): zero (or invalid) width."
fnv = sides if sides is not None else fn
shape = circle(radius=outer, fn=fnv, fa=fa, fs=fs) - circle(radius=inner, fn=fnv, fa=fa, fs=fs)
offset = _anchor_offset_box([2 * outer, 2 * outer], anchor)
return _finish(shape, offset, spin, size=[2 * outer, 2 * outer], anchor=anchor)
[docs]
def glued_circles(
radius: float | None = None,
spread: float = 10,
tangent: float = 30,
diameter: float | None = None,
anchor: Anchor | Sequence[float] = CENTER,
spin: float = 0,
fn: int | None = None,
fa: float | None = None,
fs: float | None = None,
) -> Bosl2Shape2D:
"""Two circles joined by a curved waist, like a dumbbell, built directly with polygon().
Args:
radius: radius of the end circles
spread: distance between the centers of the end circles (default 10)
tangent: angle in degrees of the tangent point of the joining arcs, from the Y axis (default 30)
diameter: diameter of the end circles (alternative to radius)
anchor: anchor point (default CENTER)
spin: Z-axis rotation in degrees after anchor (default 0)
fn: arc smoothness overrides
fa: arc smoothness overrides
fs: arc smoothness overrides
Examples:
.. pythonscad-example::
from pybosl2 import shapes2d as s2
s2.glued_circles(radius=10, spread=25, tangent=30).linear_extrude(height=5).show()
"""
rad = radius if radius is not None else (diameter / 2 if diameter is not None else 10)
cp1 = [spread / 2, 0.0]
sa1 = 90 - tangent
ea1 = 270 + tangent
lobearc = ea1 - sa1
lobesegs = math.ceil(_frag_count(rad, fn, fa, fs) * lobearc / 360)
if tangent == 0:
# radius2/cp2 (the inner waist arc) are undefined and unused in this case: the two end
# circles' own arcs already meet with no separate waist curve needed.
path = _arc_points(lobesegs + 1, rad, sa1, ea1 - sa1, [-cp1[0], -cp1[1]]) + _arc_points(
lobesegs + 1, rad, sa1 + 180, ea1 - sa1, cp1
)
else:
radius2 = (spread / 2 / math.sin(math.radians(tangent))) - rad
cp2 = [0.0, (rad + radius2) * math.cos(math.radians(tangent))]
sa2 = 270 - tangent
ea2 = 270 + tangent
subarc = ea2 - sa2
arcsegs = math.ceil(_frag_count(radius2, fn, fa, fs) * abs(subarc) / 360)
part1 = _arc_points(lobesegs, rad, sa1, ea1 - sa1, [-cp1[0], -cp1[1]], endpoint=False)
part2 = []
for k in range(arcsegs):
theta = (ea2 + 180) + k * ((ea2 - subarc + 180) - (ea2 + 180)) / arcsegs
part2.append(
[
radius2 * math.cos(math.radians(theta)) - cp2[0],
radius2 * math.sin(math.radians(theta)) - cp2[1],
]
)
part3 = _arc_points(lobesegs, rad, sa1 + 180, ea1 - sa1, cp1, endpoint=False)
part4 = []
for k in range(arcsegs):
theta = ea2 + k * ((ea2 - subarc) - ea2) / arcsegs
part4.append(
[
radius2 * math.cos(math.radians(theta)) + cp2[0],
radius2 * math.sin(math.radians(theta)) + cp2[1],
]
)
path = part1 + part2 + part3 + part4
maxx_idx = max(range(len(path)), key=lambda i: path[i][0])
path = list(reversed(path[maxx_idx:] + path[:maxx_idx]))
shape = _opolygon(path)
offset = _anchor_offset_hull(path, anchor)
return _finish(shape, offset, spin)
[docs]
def reuleaux_polygon(
sides: int = 3,
radius: float | None = None,
diameter: float | None = None,
anchor: Anchor | Sequence[float] = CENTER,
spin: float = 0,
fn: int | None = None,
fa: float | None = None,
fs: float | None = None,
) -> Bosl2Shape2D:
"""A Reuleaux polygon (constant-width curved-side shape), built directly with polygon().
Args:
sides: number of "sides"; must be an odd positive number (default 3)
radius: scale the shape to fit in a circle of this radius
diameter: scale the shape to fit in a circle of this diameter
anchor: anchor point (default CENTER)
spin: Z-axis rotation in degrees after anchor (default 0)
fn: arc smoothness overrides
fa: arc smoothness overrides
fs: arc smoothness overrides
Examples:
.. pythonscad-example::
from pybosl2 import shapes2d as s2
s2.reuleaux_polygon(sides=3, radius=15).linear_extrude(height=5).show()
"""
assert sides >= 3 and sides % 2 == 1
rad = radius if radius is not None else (diameter / 2 if diameter is not None else 1)
ssegs = max(3, math.ceil(_frag_count(rad, fn, fa, fs) / sides))
slen = math.dist(_polar_to_xy(rad, 0), _polar_to_xy(rad, 180 - 180.0 / sides))
path = []
for i in range(sides):
ca = 180 - (i + 0.5) * 360.0 / sides
sa = ca + 180 + 90.0 / sides
ea = ca + 180 - 90.0 / sides
center = _polar_to_xy(rad, ca)
path += _arc_points(ssegs - 1, slen, sa, ea - sa, center, endpoint=False)
shape = _opolygon(path)
offset = _anchor_offset_hull(path, anchor)
return _finish(shape, offset, spin)