# Copyright 2024 Gergely Bencsik
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from py_gearworks.function_generators import *
from py_gearworks.defs import *
from py_gearworks.base_classes import *
from py_gearworks.gearmath import *
import py_gearworks.curve as crv
from scipy.optimize import root
from scipy.optimize import minimize
from scipy.optimize import OptimizeResult
import dataclasses
import copy
from typing import Callable
[docs]
@dataclasses.dataclass
class FilletParam:
"""Data class for tooth tip and root modification parameters.
Attributes
----------
tip_fillet : float
Tip fillet radius coefficent.
root_fillet : float
Root fillet radius coefficent.
tip_reduction : float
Tip reduction (truncation) coefficient.
"""
tip_fillet: float = 0.0
root_fillet: float = 0.0
tip_reduction: float = 0.0
[docs]
def generate_reference_circles(
pitch_radius, limitparam: ToothLimitParam, coneparam: GearPolarTransform
) -> GearRefCircles:
"""Generates reference circles as Curve objects for a gear tooth."""
# TODO: this could be a classmethod of GearRefCircles?
p0 = RIGHT * pitch_radius
if limitparam.h_o > pitch_radius - DELTA:
h_o = pitch_radius - DELTA
else:
h_o = limitparam.h_o
pa = coneparam.inverse_polar_transform(
coneparam.polar_transform(p0) + np.array([limitparam.h_a, 0, 0])
)
pd = coneparam.inverse_polar_transform(
coneparam.polar_transform(p0) + np.array([-limitparam.h_d, 0, 0])
)
po = coneparam.inverse_polar_transform(
coneparam.polar_transform(p0) + np.array([-h_o, 0, 0])
)
rp_circle = crv.ArcCurve.from_point_center_angle(
p0=p0, center=OUT * p0[2], angle=2 * PI
)
ra_circle = crv.ArcCurve.from_point_center_angle(
p0=pa, center=OUT * pa[2], angle=2 * PI
)
rd_circle = crv.ArcCurve.from_point_center_angle(
p0=pd, center=OUT * pd[2], angle=2 * PI
)
ro_circle = crv.ArcCurve.from_point_center_angle(
p0=po, center=OUT * po[2], angle=2 * PI
)
return GearRefCircles(ra_circle, rp_circle, rd_circle, ro_circle)
[docs]
def generate_reference_lines(
limitparam: ToothLimitParam,
) -> RackRefLines:
pitch_line = crv.LineCurve(p0=UP, p1=DOWN)
addendum_line = crv.LineCurve(
p0=UP + RIGHT * limitparam.h_a, p1=UP + RIGHT * limitparam.h_a
)
dedendum_line = crv.LineCurve(
p0=UP - RIGHT * limitparam.h_d, p1=UP - RIGHT * limitparam.h_d
)
outside_line = crv.LineCurve(
p0=UP - RIGHT * limitparam.h_o, p1=UP - RIGHT * limitparam.h_o
)
return RackRefLines(
a_line=addendum_line,
d_line=dedendum_line,
o_line=outside_line,
p_line=pitch_line,
)
[docs]
def apply_tip_reduction(
tooth_curve: crv.CurveChain,
addendum_height: float,
dedendum_height: float,
tip_reduction: float,
polar_transformer: GearPolarTransform,
) -> float:
"""Apply tip reduction feature and return radius.
Checks if tip reduction is necessary (due to sharp point tip) and returns the
reduced addendum radius to be used for truncated tip.
Returns
-------
float
The reduced addendum radius."""
soldata = []
rah = addendum_height
rdh = dedendum_height
r_out = rah
for guess in np.linspace(0.1, 0.9, 4):
sol1 = crv.find_curve_plane_intersect(tooth_curve, plane_normal=UP, guess=guess)
r_sol = polar_transformer.polar_transform(tooth_curve(sol1.x[0]))[0]
if sol1.success and r_sol > rdh:
soldata.append((sol1, r_sol))
if len(soldata) > 0:
sol, r_sol = soldata[np.argmin([soltuple[1] for soltuple in soldata])]
if r_sol - tip_reduction < rah:
if tip_reduction > 0:
r_out = r_sol - tip_reduction
else:
r_out = r_sol
return r_out
[docs]
def apply_fillet(
tooth_curve: crv.CurveChain,
pitch_angle: float,
target_circle: crv.ArcCurve,
fillet_radius: float,
direction=1,
) -> crv.CurveChain:
"""Apply fillet to the tooth curve.
Parameters
----------
tooth_curve : crv.CurveChain
The tooth curve to be filleted.
pitch_angle : float
The pitch angle of the gear in radians.
target_circle : crv.ArcCurve
The circle to be filleted to. Either addendum or dedendum circle.
fillet_radius : float
The radius of the fillet.
direction : int
Use -1 for tip fillet, 1 for root fillet."""
def angle_check(p):
angle = -np.arctan2(p[1], p[0])
return 0 < angle < pitch_angle / 2
ref_circle_guess = -pitch_angle / (2 * PI) / 4 * 1.01
sol1 = crv.find_curve_intersect(
tooth_curve,
target_circle,
guess=[0.5, ref_circle_guess],
)
if sol1.success and angle_check(target_circle(sol1.x[1])):
sharp_root = False
guesses = np.asarray([0.5, 1, 1.5]) * fillet_radius
if direction == 1:
for guess in guesses:
start_locations = [
sol1.x[1] - guess / target_circle.length,
sol1.x[0] + guess / tooth_curve.length,
]
arc, t1, t2, sol = crv.calc_tangent_arc(
target_circle,
tooth_curve,
fillet_radius,
start_locations=start_locations,
)
if sol.success:
break
else:
for guess in guesses:
start_locations = [
sol1.x[0] - guess / tooth_curve.length,
sol1.x[1] + guess / target_circle.length,
]
arc, t1, t2, sol = crv.calc_tangent_arc(
tooth_curve,
target_circle,
fillet_radius,
start_locations=start_locations,
)
if sol.success:
break
if angle_check(arc(0)) and angle_check(arc(1)):
if direction == 1:
tooth_curve.set_start_on(t2)
tooth_curve.insert(0, arc)
else:
tooth_curve.set_end_on(t1)
tooth_curve.append(arc)
else:
sharp_root = True
else:
sharp_root = True
if sharp_root:
if direction == 1:
plane_normal = rotate_vector(UP, -pitch_angle / 2)
else:
plane_normal = UP
mirror_curve = crv.MirroredCurve(tooth_curve, plane_normal=plane_normal)
mirror_curve.reverse()
start_locations = [
1 - fillet_radius / tooth_curve.length,
0 + fillet_radius / tooth_curve.length,
]
if direction == 1:
arc, t1, t2, sol = crv.calc_tangent_arc(
mirror_curve,
tooth_curve,
fillet_radius,
start_locations=start_locations,
)
arc.set_start_on(0.5)
tooth_curve.set_start_on(t2)
tooth_curve.insert(0, arc)
else:
arc, t1, t2, sol = crv.calc_tangent_arc(
tooth_curve,
mirror_curve,
fillet_radius,
start_locations=start_locations,
)
arc.set_end_on(0.5)
tooth_curve.set_end_on(t1)
tooth_curve.append(arc)
return tooth_curve
[docs]
@dataclasses.dataclass
class GearRefProfile:
"""Data class collecting all curve segments for a single tooth's profile.
This class should contain all data necessary to construct a single tooth's profile,
which is 1 unit of the repeating pattern making up the gear."""
ra_curve: crv.ArcCurve
rd_curve: crv.ArcCurve
ro_curve: crv.ArcCurve
tooth_curve: crv.Curve
tooth_curve_mirror: crv.MirroredCurve
pitch_angle: float
transform: GearTransform
@property
def profile(self):
return crv.CurveChain(
self.rd_curve, self.tooth_curve, self.ra_curve, self.tooth_curve_mirror
)
[docs]
@dataclasses.dataclass
class GearRefProfileExtended(GearRefProfile):
"""Data class with additional curve segments around the tooth profile, such as
connectors to the outside ring."""
ro_connector_0: crv.Curve
ro_connector_1: crv.Curve
ro_connector_2: crv.Curve
rd_connector: crv.Curve
ra_connector: crv.Curve
ro_curve_tooth: crv.Curve
ro_curve_dedendum: crv.Curve
tooth_centerline: crv.Curve
@property
def profile_closed(self):
"""Closed curve chain that includes the tooth and the dedendum arc, returns via
the outside (or inside) ring."""
return crv.CurveChain(
self.rd_curve,
self.tooth_curve,
self.ra_curve,
self.tooth_curve_mirror,
self.ro_connector_2.copy().reverse(),
self.ro_curve.copy().reverse(),
self.ro_connector_0,
)
@property
def tooth_profile_closed(self):
"""Closed curve chain that includes just the tooth, closes at the root of the
tooth with an arc."""
return crv.CurveChain(
self.tooth_curve,
self.ra_curve,
self.tooth_curve_mirror,
self.rd_connector.copy().reverse(),
)
@property
def tooth_profile_closed_outer(self):
"""Closed curve chain that includes just the tooth as it were part of an
outside-ring gear, closing at the addendum arc."""
return crv.CurveChain(
crv.RotatedCurve(
self.tooth_curve_mirror, angle=-self.pitch_angle, axis=OUT
),
self.rd_curve,
self.tooth_curve,
self.ra_connector,
)
@property
def tooth_profile_closed_ring(self):
"""Closed curve chain that includes just the tooth, closes at the outside (or
inside) ring."""
return crv.CurveChain(
self.tooth_curve,
self.ra_connector,
self.tooth_curve_mirror,
self.ro_connector_2,
self.ro_curve_tooth.copy().reverse(),
self.ro_connector_1.copy().reverse(),
)
[docs]
@classmethod
def from_refprofile(cls, profile: GearRefProfile, cone: ConicData):
return generate_profile_extensions(profile, cone)
[docs]
def trim_reference_profile(
tooth_curve: crv.Curve,
ref_curves: GearRefCircles,
fillet: FilletParam,
pitch_angle: float,
) -> GearRefProfile:
"""Find intersections of tooth curve and reference curves and trim them."""
# if tip fillet is used, tooth curve tip is already settled
# in fact this solver tends to fail due to tangential nature of fillet
if not fillet.tip_fillet > 0:
ra_guess = -pitch_angle / 8 / ref_curves.r_a_curve.length
sol_tip = crv.find_curve_intersect(
tooth_curve,
ref_curves.r_a_curve,
guess=[0.9, ra_guess],
method=crv.IntersectMethod.EQUALITY,
)
if not sol_tip.success:
# try the other way
sol_tip = crv.find_curve_intersect(
tooth_curve,
ref_curves.r_a_curve,
guess=[0.9, ra_guess],
method=crv.IntersectMethod.MINDISTANCE,
)
solcheck = np.linalg.norm(
tooth_curve(sol_tip.x[0]) - ref_curves.r_a_curve(sol_tip.x[1])
)
if (sol_tip.success or solcheck < 1e-5) and tooth_curve(sol_tip.x[0])[1] < 0:
tooth_curve.set_end_on(sol_tip.x[0])
else:
sol_mid = crv.find_curve_plane_intersect(
tooth_curve, plane_normal=UP, guess=1
)
tooth_curve.set_end_on(sol_mid.x[0])
if not fillet.root_fillet > 0:
rd_guess = -pitch_angle / 2 / ref_curves.r_d_curve.length
sol_root = crv.find_curve_intersect(
tooth_curve,
ref_curves.r_d_curve,
guess=[0.3, rd_guess],
method=crv.IntersectMethod.EQUALITY,
)
solcheck = np.linalg.norm(
tooth_curve(sol_root.x[0]) - ref_curves.r_d_curve(sol_root.x[1])
)
if not sol_root.success:
# try the other way
sol_root_2 = crv.find_curve_intersect(
tooth_curve,
ref_curves.r_d_curve,
guess=[0, rd_guess],
method=crv.IntersectMethod.MINDISTANCE,
)
solcheck2 = np.linalg.norm(
tooth_curve(sol_root.x[0]) - ref_curves.r_d_curve(sol_root.x[1])
)
if sol_root_2.success or solcheck2 < 1e-5:
solcheck = solcheck2
sol_root = sol_root_2
# angle check: check if the found solution is within the pitch angle range
angle_check = np.arctan2(
tooth_curve(sol_root.x[0])[1], tooth_curve(sol_root.x[0])[0]
)
if (sol_root.success or solcheck < 1e-5) and angle_check > -pitch_angle / 2:
tooth_curve.set_start_on(sol_root.x[0])
else:
plane_norm = rotate_vector(UP, -pitch_angle / 2)
if sol_root.success or solcheck < 1e-5:
guess = sol_root.x[0]
else:
guess = 0.1
sol_mid2 = crv.find_curve_plane_intersect(
tooth_curve, plane_normal=plane_norm, guess=guess
)
# sol_bot = crv.find_curve_intersect(
# tooth_curve,
# ref_curves.r_d_curve,
# guess=[sol_mid2.x[0], rd_guess],
# method=crv.IntersectMethod.MINDISTANCE,
# )
sol_bot = minimize(
lambda t: np.linalg.norm(tooth_curve(t)[:2]), sol_mid2.x[0]
)
if sol_bot.x[0] > sol_mid2.x[0]:
sol_mid2 = sol_bot
tooth_curve.set_start_on(sol_mid2.x[0])
tooth_mirror = crv.MirroredCurve(tooth_curve, plane_normal=UP)
tooth_mirror.reverse()
tooth_rotate = crv.RotatedCurve(tooth_mirror, angle=-pitch_angle, axis=OUT)
pa1 = tooth_curve(1)
pa2 = tooth_mirror(0)
center_a = ((pa1 + pa2) / 2 * np.array([0, 0, 1])) * OUT
if np.linalg.norm(pa1 - pa2) > 1e-10:
ra_curve = crv.ArcCurve.from_2_point_center(p0=pa1, p1=pa2, center=center_a)
else:
ra_curve = crv.ArcCurve(
radius=ref_curves.r_a,
angle=0,
center=center_a,
yaw=np.atan2(pa1[1], pa1[0]),
active=False,
)
pd1 = tooth_curve(0)
pd2 = tooth_rotate(1)
center_d = ((pd1 + pd2) / 2 * np.array([0, 0, 1])) * OUT
if np.linalg.norm(pd1 - pd2) > 1e-10:
rd_curve = crv.ArcCurve.from_2_point_center(p0=pd2, p1=pd1, center=center_d)
else:
rd_curve = crv.ArcCurve(
radius=ref_curves.r_d,
angle=0,
center=center_d,
yaw=np.atan2(pd1[1], pd1[0]),
active=False,
)
profile = crv.CurveChain(rd_curve, tooth_curve, ra_curve, tooth_mirror)
angle_0 = np.arctan2(profile(0)[1], profile(0)[0])
angle_1 = np.arctan2(profile(1)[1], profile(1)[0])
ro_curve = crv.ArcCurve(
ref_curves.r_o_curve.r,
center=ref_curves.r_o_curve.center,
angle=angle_1 - angle_0,
yaw=angle_0,
)
return GearRefProfile(
ra_curve,
rd_curve,
ro_curve,
tooth_curve,
tooth_mirror,
pitch_angle,
GearTransform(),
)
###############################################################################
###############################################################################
[docs]
@dataclasses.dataclass
class GearProfileDataCollector:
"""
All input data collected to be able to generate 1 reference profile
for an involute gear.
"""
tooth_generator: GearToothConicGenerator
cone: ConicData
limits: ToothLimitParam
pitch_angle: float
transform: GearTransformData
fillet: FilletParam
[docs]
def generate_reference_profile(inputdata: GearProfileDataCollector) -> GearRefProfile:
"""Perform all steps to generate a single tooth profile for an involute gear."""
conic_transform = GearPolarTransform(
cone_angle=inputdata.cone.cone_angle, base_radius=inputdata.cone.base_radius
)
ref_curves = generate_reference_circles(
inputdata.tooth_generator.pitch_radius,
inputdata.limits,
conic_transform,
)
tooth_curve = inputdata.tooth_generator.generate_tooth_curve()
if inputdata.fillet.tip_reduction > 0:
r_ah = apply_tip_reduction(
tooth_curve=tooth_curve,
addendum_height=conic_transform.polar_transform(ref_curves.r_a_curve(0))[0],
dedendum_height=conic_transform.polar_transform(ref_curves.r_d_curve(0))[0],
tip_reduction=inputdata.fillet.tip_reduction,
polar_transformer=conic_transform,
)
pa = conic_transform.inverse_polar_transform(np.array([r_ah, 0, 0]))
ref_curves.r_a_curve = crv.ArcCurve.from_point_center_angle(
p0=pa, center=OUT * pa[2], angle=2 * PI
)
if inputdata.fillet.tip_fillet > 0:
if not isinstance(tooth_curve, crv.CurveChain):
tooth_curve = crv.CurveChain(tooth_curve)
tooth_curve = apply_fillet(
tooth_curve,
inputdata.pitch_angle,
ref_curves.r_a_curve,
inputdata.fillet.tip_fillet,
direction=-1,
)
if inputdata.fillet.root_fillet > 0:
if not isinstance(tooth_curve, crv.CurveChain):
tooth_curve = crv.CurveChain(tooth_curve)
tooth_curve = apply_fillet(
tooth_curve,
inputdata.pitch_angle,
ref_curves.r_d_curve,
inputdata.fillet.root_fillet,
direction=1,
)
profile = trim_reference_profile(
tooth_curve, ref_curves, inputdata.fillet, inputdata.pitch_angle
)
profile.transform = GearTransform(**(inputdata.transform.__dict__))
return profile
[docs]
def generate_profile_extensions(
profile: GearRefProfile, cone_data: ConicData
) -> GearRefProfileExtended:
"""Generate additional curve segments around the tooth profile, such as connectors"""
tooth_start_point = profile.tooth_curve(0)
tooth_start_plane_normal = np.cross(OUT, normalize_vector(tooth_start_point))
sol_ro_midpoint = crv.find_curve_plane_intersect(
profile.ro_curve, plane_normal=tooth_start_plane_normal, guess=0.5
)
ro_midpoint = profile.ro_curve(sol_ro_midpoint.x[0])
ro_curve_tooth = profile.ro_curve.copy()
ro_curve_tooth.set_start_on(sol_ro_midpoint.x[0])
ro_curve_dedendum = profile.ro_curve.copy()
ro_curve_dedendum.set_end_on(sol_ro_midpoint.x[0])
rd_connector = crv.ArcCurve.from_2_point_center(
p0=profile.tooth_curve(0),
p1=profile.tooth_curve_mirror(1),
center=profile.rd_curve.center,
)
ra_connector = crv.ArcCurve.from_2_point_center(
p0=rotate_vector(profile.ra_curve(1), -profile.pitch_angle),
p1=profile.ra_curve(0),
center=profile.ra_curve.center,
)
if cone_data.cone_angle == 0:
ro_connector_0 = crv.LineCurve(p0=profile.ro_curve(0), p1=profile.profile(0))
ro_connector_1 = crv.LineCurve(p0=ro_midpoint, p1=profile.tooth_curve(0))
ro_connector_2 = crv.LineCurve(
p0=profile.ro_curve(1), p1=profile.tooth_curve_mirror(1)
)
tooth_centerline = crv.LineCurve(p0=rd_connector(0.5), p1=profile.ra_curve(0.5))
else:
ro_connector_0 = crv.ArcCurve.from_2_point_center(
p0=profile.ro_curve(0),
p1=profile.profile(0),
center=cone_data.center,
)
ro_connector_1 = crv.ArcCurve.from_2_point_center(
p0=ro_midpoint,
p1=profile.tooth_curve(0),
center=cone_data.center,
)
ro_connector_2 = crv.ArcCurve.from_2_point_center(
p0=profile.ro_curve(1),
p1=profile.tooth_curve_mirror(1),
center=cone_data.center,
)
tooth_centerline = crv.ArcCurve.from_2_point_center(
p0=rd_connector(0.5), p1=profile.ra_curve(0.5), center=cone_data.center
)
return GearRefProfileExtended(
ra_curve=profile.ra_curve,
rd_curve=profile.rd_curve,
ro_curve=profile.ro_curve,
tooth_curve=profile.tooth_curve,
tooth_curve_mirror=profile.tooth_curve_mirror,
pitch_angle=profile.pitch_angle,
transform=profile.transform,
ro_connector_0=ro_connector_0,
ro_connector_1=ro_connector_1,
ro_connector_2=ro_connector_2,
rd_connector=rd_connector,
ra_connector=ra_connector,
ro_curve_tooth=ro_curve_tooth,
ro_curve_dedendum=ro_curve_dedendum,
tooth_centerline=tooth_centerline,
)
[docs]
def generate_profile_closed(profile: GearRefProfile, cone_data: ConicData):
"""Generate a closed profile for a gear tooth.
The profile contains 2 tooth flanks, 1 addendum and 1 dedendum curve segments,
the outside (or inside) ring curve segment, and 2 connector curves.
Ordering: dedendum->tooth->addendum->tooth_mirror->connector_1->outside_ring->connector_0.
"""
if cone_data.cone_angle == 0:
ro_connector_0 = crv.LineCurve(p0=profile.ro_curve(0), p1=profile.profile(0))
ro_connector_1 = crv.LineCurve(p1=profile.ro_curve(1), p0=profile.profile(1))
else:
ro_connector_0 = crv.ArcCurve.from_2_point_center(
p0=profile.ro_curve(0),
p1=profile.profile(0),
center=cone_data.center,
)
ro_connector_1 = crv.ArcCurve.from_2_point_center(
p1=profile.ro_curve(1),
p0=profile.profile(1),
center=cone_data.center,
)
return crv.CurveChain(
profile.profile.copy(),
ro_connector_1,
profile.ro_curve.copy().reverse(),
ro_connector_0,
)
[docs]
def generate_boundary_chain(
profile: GearRefProfile, toothdata: GearToothParam
) -> crv.CurveChain:
"""
Create gear boundary by repeating reference profile in a CurveChain.
"""
crv_list = []
for i in range(toothdata.num_teeth_act):
crv_list.append(
crv.TransformedCurve(
curve=crv.RotatedCurve(
curve=profile.profile.copy(),
angle=i * toothdata.pitch_angle,
axis=OUT,
),
transform=profile.transform,
)
)
return crv.CurveChain(*crv_list)
[docs]
def generate_boundary(profile: GearRefProfile, toothdata: GearToothParam) -> crv.Curve:
"""
Create gear boundary by defining custom repeating function for the profile.
"""
def loc_func(t, curve=profile.profile):
i = t * toothdata.num_teeth_act // 1
t2 = t * toothdata.num_teeth_act % 1
return (
curve(t2)
[docs]
@ scp_Rotation.from_euler("z", i * toothdata.pitch_angle).as_matrix().T
)
return crv.Curve(
loc_func,
t0=0,
t1=1,
params={"curve": profile.profile},
lenght_approx_ndiv=toothdata.num_teeth * 20,
)
# "Recipe" names should refer to parameter sets that define certain kinds of gears in
# 3D, eg. bevel, helical, etc. using callable parameters that represent the
# parameter value as a function of the extrusion distance z.
class GearProfileRecipe(GearProfileDataCollector, ZFunctionMixin):
pass
[docs]
class ConicDataRecipe(ConicData, ZFunctionMixin):
pass
[docs]
class ToothLimitParamRecipe(ToothLimitParam, ZFunctionMixin):
pass
[docs]
class FilletDataRecipe(FilletParam, ZFunctionMixin):
pass
[docs]
def default_gear_recipe(
teeth_data: GearToothParam,
tooth_generator: GearToothConicGenerator,
cone_angle: float = 0,
) -> GearProfileRecipe:
"""This creates the default recipe for a 3D gear tooth profile.
Recipe refers to a collection of parameter-generator callable functions that
describe how the tooth profile changes along the extrusion direction (z axis).
This function creates a recipe considering cone angle and number of teeth."""
rp_ref = teeth_data.num_teeth / 2
pitch_angle = 2 * PI / teeth_data.num_teeth
gamma = cone_angle / 2
tooth_generator.pitch_radius = rp_ref
tooth_generator.cone_angle = cone_angle
tooth_generator.pitch_intersect_angle = pitch_angle / 4
return GearProfileRecipe(
tooth_generator=tooth_generator,
cone=ConicDataRecipe(base_radius=rp_ref, cone_angle=cone_angle),
limits=ToothLimitParamRecipe(),
pitch_angle=teeth_data.pitch_angle,
transform=GearTransformRecipe(
scale=lambda z: 1 * (1 - z * 2 * np.sin(gamma) / teeth_data.num_teeth),
center=lambda z: 1 * z * OUT * np.cos(gamma),
),
fillet=FilletDataRecipe(),
)
[docs]
def gear_recipe_from_curve(
teeth_data: GearToothParam,
tooth_generator: GearToothConicGenerator,
ref_curve: crv.Curve,
ref_curve_scaling_function: Callable = lambda t: t,
gamma_rounding: float = DELTA,
) -> GearProfileRecipe:
"""This creates a recipe for a 3D gear based on a reference curve that defines
the path of 1 tooth."""
rp_ref = teeth_data.num_teeth / 2
pitch_angle = 2 * PI / teeth_data.num_teeth
def centerfunc(z):
return ref_curve_2_param(ref_curve_scaling_function(z), ref_curve).center
def gammafunc(z):
gamma_val = ref_curve_2_param(ref_curve_scaling_function(z), ref_curve).gamma
if gamma_rounding == 0:
return gamma_val
else:
return np.round(gamma_val / gamma_rounding) * gamma_rounding
def anglefunc(z):
return ref_curve_2_param(ref_curve_scaling_function(z), ref_curve).angle
def scalefunc(z):
return (
ref_curve_2_param(ref_curve_scaling_function(z), ref_curve).radius / rp_ref
)
tooth_generator.pitch_radius = rp_ref
tooth_generator.cone_angle = lambda z: gammafunc(z) * 2
tooth_generator.pitch_intersect_angle = pitch_angle / 4
return GearProfileRecipe(
tooth_generator=tooth_generator,
cone=ConicDataRecipe(base_radius=rp_ref, cone_angle=lambda z: gammafunc(z) * 2),
limits=ToothLimitParamRecipe(),
pitch_angle=teeth_data.pitch_angle,
transform=GearTransformRecipe(
scale=scalefunc,
center=centerfunc,
angle=anglefunc,
),
fillet=FilletDataRecipe(),
)
[docs]
class Gear:
"""Manager class that pulls everything together to generate a 3D gear."""
def __init__(
self,
z_vals: np.ndarray = np.array([0, 1]),
module: float = 1,
tooth_param: GearToothParam = None,
tooth_generator: GearToothGenerator = None,
shape_recipe: GearProfileRecipe = None,
transform: GearTransform = None,
cone: ConicData = None,
):
self.module = module
self.z_vals = z_vals.astype(float)
if tooth_generator is None:
# it is updated in the default recipe
self.tooth_generator = GearToothConicGenerator()
else:
self.tooth_generator = tooth_generator
if tooth_param is None:
self.tooth_param = GearToothParam()
else:
self.tooth_param = tooth_param
if cone is None:
cone = ConicData()
cone.base_radius = self.tooth_param.num_teeth / 2
if shape_recipe is None:
z_copy = copy.deepcopy(z_vals)
self.ref_curve_scaler = lambda z: (z) / (z_copy[-1] - z_copy[0])
p0 = RIGHT * self.tooth_param.num_teeth / 2
gamma_rot = scp_Rotation.from_euler("y", -cone.gamma)
p1 = p0 + gamma_rot.apply(OUT * (z_copy[-1] - z_copy[0]))
self.ref_curve = crv.LineCurve(p0=p0, p1=p1)
self.shape_recipe = gear_recipe_from_curve(
teeth_data=self.tooth_param,
tooth_generator=self.tooth_generator,
ref_curve=self.ref_curve,
ref_curve_scaling_function=self.ref_curve_scaler,
)
if cone.gamma == 0:
self.shape_recipe.cone.cone_angle = 0
self.shape_recipe.tooth_generator.cone_angle = 0
else:
self.shape_recipe = shape_recipe
if transform is None:
self.transform = GearTransform(scale=self.module)
else:
self.transform = transform
@property
def cone(self):
return self.shape_recipe(0).cone
@property
def rp(self):
return self.tooth_param.num_teeth / 2 * self.module
@property
def R(self):
return self.cone.spherical_radius * self.module
@property
def pitch_angle(self):
return self.tooth_param.pitch_angle
@property
def center(self):
return self.transform.center
@center.setter
def center(self, value):
self.transform.center = value
@property
def center_sphere(self):
return (
self.transform.center
+ self.R * np.cos(self.cone.gamma) * self.transform.z_axis
)
[docs]
def curve_gen_at_z(self, z):
return generate_reference_profile(self.shape_recipe(z))
[docs]
def sphere_data_at_z(self, z):
# trf1 = self.transform
# trf2 = self.shape_recipe(z).transform
# trf = trf1 * trf2
# cone = self.shape_recipe(z).cone
# center0 = cone.center
# center = apply_gear_transform(center0, trf)
# R = trf.scale * cone.R
cone = self.cone_at_z(z)
center = cone.center
R = cone.R
return center, R
[docs]
def cone_at_z(self, z):
trf1 = self.transform
trf2 = self.shape_recipe(z).transform
trf = trf1 * trf2
cone = self.shape_recipe(z).cone
cone.transform = trf
return cone
[docs]
def boundary_at_z(self, z, continuous=True):
if continuous:
return crv.TransformedCurve(
self.transform,
generate_boundary(self.curve_gen_at_z(z), self.tooth_param),
)
else:
return crv.TransformedCurve(
self.transform,
generate_boundary_chain(self.curve_gen_at_z(z), self.tooth_param),
)
[docs]
def copy(self) -> "Gear":
return copy.deepcopy(self)
[docs]
def swap_tooth_generator(self, tooth_generator: GearToothGenerator):
tg = copy.deepcopy(tooth_generator)
tg.pitch_radius = self.shape_recipe.tooth_generator.pitch_radius
tg.cone_angle = self.shape_recipe.tooth_generator.cone_angle
tg.pitch_intersect_angle = (
self.shape_recipe.tooth_generator.pitch_intersect_angle
)
self.tooth_generator = tg
self.shape_recipe.tooth_generator = tg
[docs]
def mesh_to(self, other: "Gear", target_dir=RIGHT):
if self.cone.cone_angle != 0 or other.cone.cone_angle != 0:
v0 = calc_bevel_gear_placement_vector(
target_dir,
self.cone,
other.cone,
self.tooth_param.inside_teeth,
other.tooth_param.inside_teeth,
)
self.transform.center = v0
self.transform.orientation = calc_mesh_orientation(
self.cone.cone_angle,
other.cone.cone_angle,
self.cone.R,
other.transform,
self.tooth_param.inside_teeth,
other.tooth_param.inside_teeth,
target_dir,
offset=0,
)
self.transform.angle = calc_mesh_angle(
self.transform,
other.transform,
self.pitch_angle,
other.pitch_angle,
gear1_inside_ring=self.tooth_param.inside_teeth,
gear2_inside_ring=other.tooth_param.inside_teeth,
)
else:
if self.tooth_param.inside_teeth or other.tooth_param.inside_teeth:
distance = np.abs(self.rp - other.rp)
else:
distance = self.rp + other.rp
v0 = target_dir * distance + other.transform.center
self.transform.center = v0
self.transform.orientation = other.transform.orientation
self.transform.angle = calc_mesh_angle(
self.transform,
other.transform,
self.pitch_angle,
other.pitch_angle,
gear1_inside_ring=self.tooth_param.inside_teeth,
gear2_inside_ring=other.tooth_param.inside_teeth,
)
[docs]
def ref_curve_2_param(t, ref_curve: crv.Curve) -> RecipeKeyParams:
"""Calculate key parameters of a gear tooth profile recipe based on a reference
curve. Consider the reference curve the path of 1 tooth."""
p0 = ref_curve(t)
polar_res = xyz_to_cylindrical(p0)
radius = polar_res[0]
center = polar_res[2] * OUT
angle = polar_res[1]
diff = xyz_to_cylindrical(ref_curve(t + DELTA)) - xyz_to_cylindrical(
ref_curve(t - DELTA)
)
gamma = np.arctan2(-diff[0], diff[2])
beta = np.arctan2(diff[1] * radius, np.sqrt(diff[2] ** 2 + diff[0] ** 2))
return RecipeKeyParams(gamma, center[2], angle, radius, beta)