178 lines
5.1 KiB
Python
178 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
from math import cos, hypot, sin, tau
|
|
from typing import Iterable
|
|
|
|
from build123d import Axis, BuildPart, Color, Location, Mode, chamfer, fillet
|
|
|
|
|
|
TOLERANCE = 1e-6
|
|
|
|
|
|
def polar_point(radius: float, angle: float) -> tuple[float, float]:
|
|
return (radius * cos(angle), radius * sin(angle))
|
|
|
|
|
|
def safe_fillet(part, edges: Iterable, radius: float):
|
|
selected = list(edges)
|
|
if not selected:
|
|
return part
|
|
try:
|
|
return fillet(selected, radius=radius)
|
|
except Exception:
|
|
return part
|
|
|
|
|
|
def safe_chamfer(part, edges: Iterable, length: float):
|
|
selected = list(edges)
|
|
if not selected:
|
|
return part
|
|
try:
|
|
return chamfer(selected, length=length)
|
|
except Exception:
|
|
return part
|
|
|
|
|
|
def line_edges_at_z(part, z_value: float, *, tol: float = 0.02):
|
|
edges = []
|
|
for edge in part.edges():
|
|
bbox = edge.bounding_box()
|
|
if abs(bbox.min.Z - z_value) <= tol and abs(bbox.max.Z - z_value) <= tol:
|
|
if str(edge.geom_type).endswith("LINE"):
|
|
edges.append(edge)
|
|
return edges
|
|
|
|
|
|
def circular_edges(
|
|
part,
|
|
*,
|
|
radius: float | None = None,
|
|
axis: str | None = None,
|
|
coordinate: float | None = None,
|
|
tol: float = 0.05,
|
|
):
|
|
edges = []
|
|
for edge in part.edges():
|
|
try:
|
|
edge_radius = edge.radius
|
|
edge_center = edge.arc_center
|
|
edge_normal = edge.normal()
|
|
except Exception:
|
|
continue
|
|
|
|
if radius is not None and abs(edge_radius - radius) > tol:
|
|
continue
|
|
|
|
if axis is not None:
|
|
components = {
|
|
"x": abs(edge_normal.X),
|
|
"y": abs(edge_normal.Y),
|
|
"z": abs(edge_normal.Z),
|
|
}
|
|
if components[axis] < 1.0 - 1e-4:
|
|
continue
|
|
|
|
if coordinate is not None:
|
|
coord = {"x": edge_center.X, "y": edge_center.Y, "z": edge_center.Z}[axis or "z"]
|
|
if abs(coord - coordinate) > tol:
|
|
continue
|
|
|
|
edges.append(edge)
|
|
|
|
return edges
|
|
|
|
|
|
def cylinder_axis_x(radius: float, length: float, center: tuple[float, float, float], *, mode=Mode.ADD):
|
|
from build123d import Align, Cylinder
|
|
|
|
return Cylinder(
|
|
radius=radius,
|
|
height=length,
|
|
rotation=(0.0, 90.0, 0.0),
|
|
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
|
mode=mode,
|
|
).locate(Location(center))
|
|
|
|
|
|
def cylinder_axis_y(radius: float, length: float, center: tuple[float, float, float], *, mode=Mode.ADD):
|
|
from build123d import Align, Cylinder
|
|
|
|
return Cylinder(
|
|
radius=radius,
|
|
height=length,
|
|
rotation=(90.0, 0.0, 0.0),
|
|
align=(Align.CENTER, Align.CENTER, Align.CENTER),
|
|
mode=mode,
|
|
).locate(Location(center))
|
|
|
|
|
|
def rounded_triangle_points(
|
|
vertices: list[tuple[float, float]],
|
|
*,
|
|
radius: float,
|
|
segments: int = 5,
|
|
) -> list[tuple[float, float]]:
|
|
"""Approximate a rounded triangular loop with quadratic corner arcs."""
|
|
points: list[tuple[float, float]] = []
|
|
count = len(vertices)
|
|
|
|
for index, vertex in enumerate(vertices):
|
|
previous = vertices[(index - 1) % count]
|
|
next_vertex = vertices[(index + 1) % count]
|
|
|
|
to_previous = (previous[0] - vertex[0], previous[1] - vertex[1])
|
|
to_next = (next_vertex[0] - vertex[0], next_vertex[1] - vertex[1])
|
|
previous_length = hypot(*to_previous)
|
|
next_length = hypot(*to_next)
|
|
offset = min(radius, previous_length * 0.35, next_length * 0.35)
|
|
|
|
start = (
|
|
vertex[0] + to_previous[0] / previous_length * offset,
|
|
vertex[1] + to_previous[1] / previous_length * offset,
|
|
)
|
|
end = (
|
|
vertex[0] + to_next[0] / next_length * offset,
|
|
vertex[1] + to_next[1] / next_length * offset,
|
|
)
|
|
|
|
for step in range(segments + 1):
|
|
t = step / segments
|
|
x_pos = (1.0 - t) ** 2 * start[0] + 2.0 * (1.0 - t) * t * vertex[0] + t**2 * end[0]
|
|
y_pos = (1.0 - t) ** 2 * start[1] + 2.0 * (1.0 - t) * t * vertex[1] + t**2 * end[1]
|
|
points.append((x_pos, y_pos))
|
|
|
|
return points
|
|
|
|
|
|
def trapezoid_tooth_profile(
|
|
*,
|
|
teeth: int,
|
|
root_radius: float,
|
|
tip_radius: float,
|
|
phase: float,
|
|
root_span_fraction: float = 0.72,
|
|
tip_span_fraction: float = 0.38,
|
|
) -> list[tuple[float, float]]:
|
|
points: list[tuple[float, float]] = []
|
|
pitch_angle = tau / teeth
|
|
|
|
for tooth_index in range(teeth):
|
|
center_angle = phase + tooth_index * pitch_angle
|
|
points.extend(
|
|
(
|
|
polar_point(root_radius, center_angle - root_span_fraction * pitch_angle / 2.0),
|
|
polar_point(tip_radius, center_angle - tip_span_fraction * pitch_angle / 2.0),
|
|
polar_point(tip_radius, center_angle + tip_span_fraction * pitch_angle / 2.0),
|
|
polar_point(root_radius, center_angle + root_span_fraction * pitch_angle / 2.0),
|
|
)
|
|
)
|
|
|
|
return points
|
|
|
|
|
|
def label(part, name: str, color: Color | None = None):
|
|
part.label = name
|
|
if color is not None:
|
|
part.color = color
|
|
return part
|