5 Commits

Author SHA1 Message Date
looooo
85ae781b73 add basegear and keep feature.py 2024-01-02 02:02:20 +01:00
looooo
0ea2f229b7 Revert "rename features to basegear"
This reverts commit 878811ae54.
2024-01-02 01:53:29 +01:00
looooo
878811ae54 rename features to basegear 2024-01-02 01:37:03 +01:00
looooo
0d10df1997 ruff formating 2024-01-02 01:28:04 +01:00
looooo
7c747bf151 refactoring (split in different files)
make explicitly dependent on scipy
2024-01-02 01:25:46 +01:00
19 changed files with 3279 additions and 2439 deletions

File diff suppressed because one or more lines are too long

672
freecad/gears/basegear.py Normal file
View File

@@ -0,0 +1,672 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import os
import sys
import FreeCAD as App
import Part
import numpy as np
import math
from pygears import __version__
from pygears.involute_tooth import InvoluteTooth, InvoluteRack
from pygears.cycloid_tooth import CycloidTooth
from pygears.bevel_tooth import BevelTooth
from pygears._functions import (
rotation3D,
rotation,
reflection,
arc_from_points_and_center,
)
def fcvec(x):
if len(x) == 2:
return App.Vector(x[0], x[1], 0)
else:
return App.Vector(x[0], x[1], x[2])
class ViewProviderGear(object):
def __init__(self, obj, icon_fn=None):
# Set this object to the proxy object of the actual view provider
obj.Proxy = self
self._check_attr()
dirname = os.path.dirname(__file__)
self.icon_fn = icon_fn or os.path.join(dirname, "icons", "involutegear.svg")
def _check_attr(self):
"""Check for missing attributes."""
if not hasattr(self, "icon_fn"):
setattr(
self,
"icon_fn",
os.path.join(os.path.dirname(__file__), "icons", "involutegear.svg"),
)
def attach(self, vobj):
self.vobj = vobj
def getIcon(self):
self._check_attr()
return self.icon_fn
if sys.version_info[0] == 3 and sys.version_info[1] >= 11:
def dumps(self):
self._check_attr()
return {"icon_fn": self.icon_fn}
def loads(self, state):
if state and "icon_fn" in state:
self.icon_fn = state["icon_fn"]
else:
def __getstate__(self):
self._check_attr()
return {"icon_fn": self.icon_fn}
def __setstate__(self, state):
if state and "icon_fn" in state:
self.icon_fn = state["icon_fn"]
class BaseGear(object):
def __init__(self, obj):
obj.addProperty(
"App::PropertyString", "version", "version", "freecad.gears-version", 1
)
obj.version = __version__
self.make_attachable(obj)
def make_attachable(self, obj):
# Needed to make this object "attachable",
# aka able to attach parameterically to other objects
# cf. https://wiki.freecadweb.org/Scripted_objects_with_attachment
if int(App.Version()[1]) >= 19:
obj.addExtension("Part::AttachExtensionPython")
else:
obj.addExtension("Part::AttachExtensionPython", obj)
# unveil the "Placement" property, which seems hidden by default in PartDesign
obj.setEditorMode("Placement", 0) # non-readonly non-hidden
def execute(self, fp):
# checksbackwardcompatibility:
if not hasattr(fp, "positionBySupport"):
self.make_attachable(fp)
fp.positionBySupport()
gear_shape = self.generate_gear_shape(fp)
if hasattr(fp, "BaseFeature") and fp.BaseFeature != None:
# we're inside a PartDesign Body, thus need to fuse with the base feature
gear_shape.Placement = (
fp.Placement
) # ensure the gear is placed correctly before fusing
result_shape = fp.BaseFeature.Shape.fuse(gear_shape)
result_shape.transformShape(
fp.Placement.inverse().toMatrix(), True
) # account for setting fp.Shape below moves the shape to fp.Placement, ignoring its previous placement
fp.Shape = result_shape
else:
fp.Shape = gear_shape
def generate_gear_shape(self, fp):
"""
This method has to return the TopoShape of the gear.
"""
raise NotImplementedError("generate_gear_shape not implemented")
if sys.version_info[0] == 3 and sys.version_info[1] >= 11:
def loads(self, state):
pass
def dumps(self):
pass
else:
def __setstate__(self, state):
pass
def __getstate__(self):
pass
class LanternGear(BaseGear):
def __init__(self, obj):
super(LanternGear, self).__init__(obj)
obj.addProperty(
"App::PropertyInteger", "teeth", "gear_parameter", "number of teeth"
)
obj.addProperty("App::PropertyLength", "module", "base", "module")
obj.addProperty(
"App::PropertyLength",
"bolt_radius",
"base",
"the bolt radius of the rack/chain",
)
obj.addProperty("App::PropertyLength", "height", "base", "height")
obj.addProperty(
"App::PropertyInteger",
"num_profiles",
"accuracy",
"number of profiles used for loft",
)
obj.addProperty(
"App::PropertyFloat",
"head",
"tolerance",
"head * module = additional length of head",
)
obj.teeth = 15
obj.module = "1. mm"
obj.bolt_radius = "1 mm"
obj.height = "5. mm"
obj.num_profiles = 10
self.obj = obj
obj.Proxy = self
def generate_gear_shape(self, fp):
m = fp.module.Value
teeth = fp.teeth
r_r = fp.bolt_radius.Value
r_0 = m * teeth / 2
r_max = r_0 + r_r + fp.head * m
phi_max = (r_r + np.sqrt(r_max**2 - r_0**2)) / r_0
def find_phi_min(phi_min):
return r_0 * (
phi_min**2 * r_0
- 2 * phi_min * r_0 * np.sin(phi_min)
- 2 * phi_min * r_r
- 2 * r_0 * np.cos(phi_min)
+ 2 * r_0
+ 2 * r_r * np.sin(phi_min)
)
try:
import scipy.optimize
phi_min = scipy.optimize.root(
find_phi_min, (phi_max + r_r / r_0 * 4) / 5
).x[0] # , r_r / r_0, phi_max)
except ImportError:
App.Console.PrintWarning(
"scipy not available. Can't compute numerical root. Leads to a wrong bolt-radius"
)
phi_min = r_r / r_0
# phi_min = 0 # r_r / r_0
phi = np.linspace(phi_min, phi_max, fp.num_profiles)
x = r_0 * (np.cos(phi) + phi * np.sin(phi)) - r_r * np.sin(phi)
y = r_0 * (np.sin(phi) - phi * np.cos(phi)) + r_r * np.cos(phi)
xy1 = np.array([x, y]).T
p_1 = xy1[0]
p_1_end = xy1[-1]
bsp_1 = Part.BSplineCurve()
bsp_1.interpolate(list(map(fcvec, xy1)))
w_1 = bsp_1.toShape()
xy2 = xy1 * np.array([1.0, -1.0])
p_2 = xy2[0]
p_2_end = xy2[-1]
bsp_2 = Part.BSplineCurve()
bsp_2.interpolate(list(map(fcvec, xy2)))
w_2 = bsp_2.toShape()
p_12 = np.array([r_0 - r_r, 0.0])
arc = Part.Arc(
App.Vector(*p_1, 0.0), App.Vector(*p_12, 0.0), App.Vector(*p_2, 0.0)
).toShape()
rot = rotation(-np.pi * 2 / teeth)
p_3 = rot(np.array([p_2_end]))[0]
# l = Part.LineSegment(fcvec(p_1_end), fcvec(p_3)).toShape()
l = part_arc_from_points_and_center(
p_1_end, p_3, np.array([0.0, 0.0])
).toShape()
w = Part.Wire([w_2, arc, w_1, l])
wires = [w]
rot = App.Matrix()
for _ in range(teeth - 1):
rot.rotateZ(np.pi * 2 / teeth)
wires.append(w.transformGeometry(rot))
wi = Part.Wire(wires)
if fp.height.Value == 0:
return wi
else:
return Part.Face(wi).extrude(App.Vector(0, 0, fp.height))
class HypoCycloidGear(BaseGear):
"""parameters:
pressure_angle: pressureangle, 10-30°
pitch_angle: cone angle, 0 < pitch_angle < pi/4
"""
def __init__(self, obj):
super(HypoCycloidGear, self).__init__(obj)
obj.addProperty(
"App::PropertyFloat",
"pin_circle_radius",
"gear_parameter",
"Pin ball circle radius(overrides Tooth Pitch",
)
obj.addProperty(
"App::PropertyFloat", "roller_diameter", "gear_parameter", "Roller Diameter"
)
obj.addProperty(
"App::PropertyFloat", "eccentricity", "gear_parameter", "Eccentricity"
)
obj.addProperty(
"App::PropertyAngle",
"pressure_angle_lim",
"gear_parameter",
"Pressure angle limit",
)
obj.addProperty(
"App::PropertyFloat",
"pressure_angle_offset",
"gear_parameter",
"Offset in pressure angle",
)
obj.addProperty(
"App::PropertyInteger",
"teeth_number",
"gear_parameter",
"Number of teeth in Cam",
)
obj.addProperty(
"App::PropertyInteger",
"segment_count",
"gear_parameter",
"Number of points used for spline interpolation",
)
obj.addProperty(
"App::PropertyLength",
"hole_radius",
"gear_parameter",
"Center hole's radius",
)
obj.addProperty(
"App::PropertyBool", "show_pins", "Pins", "Create pins in place"
)
obj.addProperty("App::PropertyLength", "pin_height", "Pins", "height")
obj.addProperty(
"App::PropertyBool",
"center_pins",
"Pins",
"Center pin Z axis to generated disks",
)
obj.addProperty(
"App::PropertyBool", "show_disk0", "Disks", "Show main cam disk"
)
obj.addProperty(
"App::PropertyBool",
"show_disk1",
"Disks",
"Show another reversed cam disk on top",
)
obj.addProperty("App::PropertyLength", "disk_height", "Disks", "height")
obj.pin_circle_radius = 66
obj.roller_diameter = 3
obj.eccentricity = 1.5
obj.pressure_angle_lim = "50.0 deg"
obj.pressure_angle_offset = 0.01
obj.teeth_number = 42
obj.segment_count = 42
obj.hole_radius = "30. mm"
obj.show_pins = True
obj.pin_height = "20. mm"
obj.center_pins = True
obj.show_disk0 = True
obj.show_disk1 = True
obj.disk_height = "10. mm"
self.obj = obj
obj.Proxy = self
def to_polar(self, x, y):
return (x**2 + y**2) ** 0.5, math.atan2(y, x)
def to_rect(self, r, a):
return r * math.cos(a), r * math.sin(a)
def calcyp(self, p, a, e, n):
return math.atan(math.sin(n * a) / (math.cos(n * a) + (n * p) / (e * (n + 1))))
def calc_x(self, p, d, e, n, a):
return (
(n * p) * math.cos(a)
+ e * math.cos((n + 1) * a)
- d / 2 * math.cos(self.calcyp(p, a, e, n) + a)
)
def calc_y(self, p, d, e, n, a):
return (
(n * p) * math.sin(a)
+ e * math.sin((n + 1) * a)
- d / 2 * math.sin(self.calcyp(p, a, e, n) + a)
)
def calc_pressure_angle(self, p, d, n, a):
ex = 2**0.5
r3 = p * n
rg = r3 / ex
pp = rg * (ex**2 + 1 - 2 * ex * math.cos(a)) ** 0.5 - d / 2
return math.asin((r3 * math.cos(a) - rg) / (pp + d / 2)) * 180 / math.pi
def calc_pressure_limit(self, p, d, e, n, a):
ex = 2**0.5
r3 = p * n
rg = r3 / ex
q = (r3**2 + rg**2 - 2 * r3 * rg * math.cos(a)) ** 0.5
x = rg - e + (q - d / 2) * (r3 * math.cos(a) - rg) / q
y = (q - d / 2) * r3 * math.sin(a) / q
return (x**2 + y**2) ** 0.5
def check_limit(self, x, y, maxrad, minrad, offset):
r, a = self.to_polar(x, y)
if (r > maxrad) or (r < minrad):
r = r - offset
x, y = self.to_rect(r, a)
return x, y
def generate_gear_shape(self, fp):
b = fp.pin_circle_radius
d = fp.roller_diameter
e = fp.eccentricity
n = fp.teeth_number
p = b / n
s = fp.segment_count
ang = fp.pressure_angle_lim
c = fp.pressure_angle_offset
q = 2 * math.pi / float(s)
# Find the pressure angle limit circles
minAngle = -1.0
maxAngle = -1.0
for i in range(0, 180):
x = self.calc_pressure_angle(p, d, n, i * math.pi / 180.0)
if (x < ang) and (minAngle < 0):
minAngle = float(i)
if (x < -ang) and (maxAngle < 0):
maxAngle = float(i - 1)
minRadius = self.calc_pressure_limit(p, d, e, n, minAngle * math.pi / 180.0)
maxRadius = self.calc_pressure_limit(p, d, e, n, maxAngle * math.pi / 180.0)
# unused
# Part.Wire(Part.makeCircle(minRadius,App.Vector(-e, 0, 0)))
# Part.Wire(Part.makeCircle(maxRadius,App.Vector(-e, 0, 0)))
App.Console.PrintMessage("Generating cam disk\r\n")
# generate the cam profile - note: shifted in -x by eccentricicy amount
i = 0
x = self.calc_x(p, d, e, n, q * i / float(n))
y = self.calc_y(p, d, e, n, q * i / n)
x, y = self.check_limit(x, y, maxRadius, minRadius, c)
points = [App.Vector(x - e, y, 0)]
for i in range(0, s):
x = self.calc_x(p, d, e, n, q * (i + 1) / n)
y = self.calc_y(p, d, e, n, q * (i + 1) / n)
x, y = self.check_limit(x, y, maxRadius, minRadius, c)
points.append([x - e, y, 0])
wi = make_bspline_wire([points])
wires = []
mat = App.Matrix()
mat.move(App.Vector(e, 0.0, 0.0))
mat.rotateZ(2 * np.pi / n)
mat.move(App.Vector(-e, 0.0, 0.0))
for _ in range(n):
wi = wi.transformGeometry(mat)
wires.append(wi)
cam = Part.Face(Part.Wire(wires))
# add a circle in the center of the cam
if fp.hole_radius.Value:
centerCircle = Part.Face(
Part.Wire(Part.makeCircle(fp.hole_radius.Value, App.Vector(-e, 0, 0)))
)
cam = cam.cut(centerCircle)
to_be_fused = []
if fp.show_disk0 == True:
if fp.disk_height.Value == 0:
to_be_fused.append(cam)
else:
to_be_fused.append(cam.extrude(App.Vector(0, 0, fp.disk_height.Value)))
# secondary cam disk
if fp.show_disk1 == True:
App.Console.PrintMessage("Generating secondary cam disk\r\n")
second_cam = cam.copy()
mat = App.Matrix()
mat.rotateZ(np.pi)
mat.move(App.Vector(-e, 0, 0))
if n % 2 == 0:
mat.rotateZ(np.pi / n)
mat.move(App.Vector(e, 0, 0))
second_cam = second_cam.transformGeometry(mat)
if fp.disk_height.Value == 0:
to_be_fused.append(second_cam)
else:
to_be_fused.append(
second_cam.extrude(App.Vector(0, 0, -fp.disk_height.Value))
)
# pins
if fp.show_pins == True:
App.Console.PrintMessage("Generating pins\r\n")
pins = []
for i in range(0, n + 1):
x = p * n * math.cos(2 * math.pi / (n + 1) * i)
y = p * n * math.sin(2 * math.pi / (n + 1) * i)
pins.append(Part.Wire(Part.makeCircle(d / 2, App.Vector(x, y, 0))))
pins = Part.Face(pins)
z_offset = -fp.pin_height.Value / 2
if fp.center_pins == True:
if fp.show_disk0 == True and fp.show_disk1 == False:
z_offset += fp.disk_height.Value / 2
elif fp.show_disk0 == False and fp.show_disk1 == True:
z_offset += -fp.disk_height.Value / 2
# extrude
if z_offset != 0:
pins.translate(App.Vector(0, 0, z_offset))
if fp.pin_height != 0:
pins = pins.extrude(App.Vector(0, 0, fp.pin_height.Value))
to_be_fused.append(pins)
if to_be_fused:
return Part.makeCompound(to_be_fused)
def part_arc_from_points_and_center(p_1, p_2, m):
p_1, p_12, p_2 = arc_from_points_and_center(p_1, p_2, m)
return Part.Arc(
App.Vector(*p_1, 0.0), App.Vector(*p_12, 0.0), App.Vector(*p_2, 0.0)
)
def helicalextrusion(face, height, angle, double_helix=False):
"""
A helical extrusion using the BRepOffsetAPI
face -- the face to extrude (may contain holes, i.e. more then one wires)
height -- the height of the extrusion, normal to the face
angle -- the twist angle of the extrusion in radians
returns a solid
"""
pitch = height * 2 * np.pi / abs(angle)
radius = 10.0 # as we are only interested in the "twist", we take an arbitrary constant here
cone_angle = 0
direction = bool(angle < 0)
if double_helix:
spine = Part.makeHelix(pitch, height / 2.0, radius, cone_angle, direction)
spine.translate(App.Vector(0, 0, height / 2.0))
face = face.translated(
App.Vector(0, 0, height / 2.0)
) # don't transform our argument
else:
spine = Part.makeHelix(pitch, height, radius, cone_angle, direction)
def make_pipe(path, profile):
"""
returns (shell, last_wire)
"""
mkPS = Part.BRepOffsetAPI.MakePipeShell(path)
mkPS.setFrenetMode(
True
) # otherwise, the profile's normal would follow the path
mkPS.add(profile, False, False)
mkPS.build()
return (mkPS.shape(), mkPS.lastShape())
shell_faces = []
top_wires = []
for wire in face.Wires:
pipe_shell, top_wire = make_pipe(spine, wire)
shell_faces.extend(pipe_shell.Faces)
top_wires.append(top_wire)
top_face = Part.Face(top_wires)
shell_faces.append(top_face)
if double_helix:
origin = App.Vector(0, 0, height / 2.0)
xy_normal = App.Vector(0, 0, 1)
mirror_xy = lambda f: f.mirror(origin, xy_normal)
bottom_faces = list(map(mirror_xy, shell_faces))
shell_faces.extend(bottom_faces)
# TODO: why the heck is makeShell from this empty after mirroring?
# ... and why the heck does it work when making an intermediate compound???
hacky_intermediate_compound = Part.makeCompound(shell_faces)
shell_faces = hacky_intermediate_compound.Faces
else:
shell_faces.append(face) # the bottom is what we extruded
shell = Part.makeShell(shell_faces)
# shell.sewShape() # fill gaps that may result from accumulated tolerances. Needed?
# shell = shell.removeSplitter() # refine. Needed?
return Part.makeSolid(shell)
def make_face(edge1, edge2):
v1, v2 = edge1.Vertexes
v3, v4 = edge2.Vertexes
e1 = Part.Wire(edge1)
e2 = Part.LineSegment(v1.Point, v3.Point).toShape().Edges[0]
e3 = edge2
e4 = Part.LineSegment(v4.Point, v2.Point).toShape().Edges[0]
w = Part.Wire([e3, e4, e1, e2])
return Part.Face(w)
def make_bspline_wire(pts):
wi = []
for i in pts:
out = Part.BSplineCurve()
out.interpolate(list(map(fcvec, i)))
wi.append(out.toShape())
return Part.Wire(wi)
def points_to_wire(pts):
wire = []
for i in pts:
if len(i) == 2:
# straight edge
out = Part.LineSegment(*list(map(fcvec, i)))
else:
out = Part.BSplineCurve()
out.interpolate(list(map(fcvec, i)))
wire.append(out.toShape())
return Part.Wire(wire)
def rotate_tooth(base_tooth, num_teeth):
rot = App.Matrix()
rot.rotateZ(2 * np.pi / num_teeth)
flat_shape = [base_tooth]
for t in range(num_teeth - 1):
flat_shape.append(flat_shape[-1].transformGeometry(rot))
return Part.Wire(flat_shape)
def fillet_between_edges(edge_1, edge_2, radius):
# assuming edges are in a plane
# extracting vertices
try:
from Part import ChFi2d
except ImportError:
App.Console.PrintWarning(
"Your freecad version has no python bindings for 2d-fillets"
)
return [edge_1, edge_2]
api = ChFi2d.FilletAPI()
p1 = edge_1.valueAt(edge_1.FirstParameter)
p2 = edge_1.valueAt(edge_1.LastParameter)
p3 = edge_2.valueAt(edge_2.FirstParameter)
p4 = edge_2.valueAt(edge_2.LastParameter)
t1 = p2 - p1
t2 = p4 - p3
n = t1.cross(t2)
pln = Part.Plane(edge_1.valueAt(edge_1.FirstParameter), n)
api.init(edge_1, edge_2, pln)
if api.perform(radius) > 0:
p0 = (p2 + p3) / 2
fillet, e1, e2 = api.result(p0)
return Part.Wire([e1, fillet, e2]).Edges
else:
return None
def insert_fillet(edges, pos, radius):
assert pos < (len(edges) - 1)
e1 = edges[pos]
e2 = edges[pos + 1]
if radius > 0:
fillet_edges = fillet_between_edges(e1, e2, radius)
if not fillet_edges:
raise RuntimeError("fillet not possible")
else:
fillet_edges = [e1, None, e2]
output_edges = []
for i, edge in enumerate(edges):
if i == pos:
output_edges += fillet_edges
elif i == (pos + 1):
pass
else:
output_edges.append(edge)
return output_edges

206
freecad/gears/bevelgear.py Normal file
View File

@@ -0,0 +1,206 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import FreeCAD as App
import Part
import numpy as np
from pygears.bevel_tooth import BevelTooth
from pygears._functions import rotation3D
from .basegear import BaseGear, fcvec, make_bspline_wire
class BevelGear(BaseGear):
"""parameters:
pressure_angle: pressureangle, 10-30°
pitch_angle: cone angle, 0 < pitch_angle < pi/4
"""
def __init__(self, obj):
super(BevelGear, self).__init__(obj)
self.bevel_tooth = BevelTooth()
obj.addProperty("App::PropertyInteger", "teeth", "base", "number of teeth")
obj.addProperty("App::PropertyLength", "height", "base", "height")
obj.addProperty("App::PropertyAngle", "pitch_angle", "involute", "pitch_angle")
obj.addProperty(
"App::PropertyAngle",
"pressure_angle",
"involute_parameter",
"pressure_angle",
)
obj.addProperty("App::PropertyLength", "module", "base", "module")
obj.addProperty("App::PropertyFloat", "clearance", "tolerance", "clearance")
obj.addProperty(
"App::PropertyInteger",
"numpoints",
"precision",
"number of points for spline",
)
obj.addProperty(
"App::PropertyBool",
"reset_origin",
"base",
"if value is true the gears outer face will match the z=0 plane",
)
obj.addProperty(
"App::PropertyLength",
"backlash",
"tolerance",
"The arc length on the pitch circle by which the tooth thicknes is reduced.",
)
obj.addProperty("App::PropertyPythonObject", "gear", "base", "test")
obj.addProperty(
"App::PropertyAngle", "beta", "helical", "angle used for spiral bevel-gears"
)
obj.addProperty("App::PropertyLength", "dw", "computed", "The pitch diameter.")
obj.setExpression(
"dw", "teeth * module"
) # calculate via expression to ease usage for placement
obj.setEditorMode(
"dw", 1
) # set read-only after setting the expression, else it won't be visible. bug?
obj.addProperty(
"App::PropertyAngle",
"angular_backlash",
"computed",
"The angle by which this gear can turn without moving the mating gear.",
)
obj.setExpression(
"angular_backlash", "backlash / dw * 360° / pi"
) # calculate via expression to ease usage for placement
obj.setEditorMode(
"angular_backlash", 1
) # set read-only after setting the expression, else it won't be visible. bug?
obj.gear = self.bevel_tooth
obj.module = "1. mm"
obj.teeth = 15
obj.pressure_angle = "20. deg"
obj.pitch_angle = "45. deg"
obj.height = "5. mm"
obj.numpoints = 6
obj.backlash = "0.00 mm"
obj.clearance = 0.1
obj.beta = "0 deg"
obj.reset_origin = True
self.obj = obj
obj.Proxy = self
def generate_gear_shape(self, fp):
fp.gear.z = fp.teeth
fp.gear.module = fp.module.Value
fp.gear.pressure_angle = (90 - fp.pressure_angle.Value) * np.pi / 180.0
fp.gear.pitch_angle = fp.pitch_angle.Value * np.pi / 180
max_height = fp.gear.module * fp.teeth / 2 / np.tan(fp.gear.pitch_angle)
if fp.height >= max_height:
App.Console.PrintWarning(
"height must be smaller than {}".format(max_height)
)
fp.gear.backlash = fp.backlash.Value
scale = (
fp.module.Value * fp.gear.z / 2 / np.tan(fp.pitch_angle.Value * np.pi / 180)
)
fp.gear.clearance = fp.clearance / scale
fp.gear._update()
pts = list(fp.gear.points(num=fp.numpoints))
rot = rotation3D(2 * np.pi / fp.teeth)
# if fp.beta.Value != 0:
# pts = [np.array([self.spherical_rot(j, fp.beta.Value * np.pi / 180.) for j in i]) for i in pts]
rotated_pts = pts
for i in range(fp.gear.z - 1):
rotated_pts = list(map(rot, rotated_pts))
pts.append(np.array([pts[-1][-1], rotated_pts[0][0]]))
pts += rotated_pts
pts.append(np.array([pts[-1][-1], pts[0][0]]))
wires = []
if not "version" in fp.PropertiesList:
scale_0 = scale - fp.height.Value / 2
scale_1 = scale + fp.height.Value / 2
else: # starting with version 0.0.2
scale_0 = scale - fp.height.Value
scale_1 = scale
if fp.beta.Value == 0:
wires.append(make_bspline_wire([scale_0 * p for p in pts]))
wires.append(make_bspline_wire([scale_1 * p for p in pts]))
else:
for scale_i in np.linspace(scale_0, scale_1, 20):
# beta_i = (scale_i - scale_0) * fp.beta.Value * np.pi / 180
# rot = rotation3D(beta_i)
# points = [rot(pt) * scale_i for pt in pts]
angle = (
fp.beta.Value
* np.pi
/ 180.0
* np.sin(np.pi / 4)
/ np.sin(fp.pitch_angle.Value * np.pi / 180.0)
)
points = [
np.array([self.spherical_rot(p, angle) for p in scale_i * pt])
for pt in pts
]
wires.append(make_bspline_wire(points))
shape = Part.makeLoft(wires, True)
if fp.reset_origin:
mat = App.Matrix()
mat.A33 = -1
mat.move(fcvec([0, 0, scale_1]))
shape = shape.transformGeometry(mat)
return shape
# return self.create_teeth(pts, pos1, fp.teeth)
def create_tooth(self):
w = []
scal1 = (
self.obj.m.Value
* self.obj.gear.z
/ 2
/ np.tan(self.obj.pitch_angle.Value * np.pi / 180)
- self.obj.height.Value / 2
)
scal2 = (
self.obj.m.Value
* self.obj.gear.z
/ 2
/ np.tan(self.obj.pitch_angle.Value * np.pi / 180)
+ self.obj.height.Value / 2
)
s = [scal1, scal2]
pts = self.obj.gear.points(num=self.obj.numpoints)
for j, pos in enumerate(s):
w1 = []
def scale(x):
return fcvec(x * pos)
for i in pts:
i_scale = list(map(scale, i))
w1.append(i_scale)
w.append(w1)
surfs = []
w_t = zip(*w)
for i in w_t:
b = Part.BSplineSurface()
b.interpolate(i)
surfs.append(b)
return Part.Shape(surfs)
def spherical_rot(self, point, phi):
new_phi = np.sqrt(np.linalg.norm(point)) * phi
return rotation3D(new_phi)(point)

View File

@@ -20,22 +20,23 @@ import os
import FreeCAD
import FreeCADGui as Gui
from .features import (
from .basegear import (
ViewProviderGear,
InvoluteGear,
InternalInvoluteGear,
InvoluteGearRack,
CycloidGearRack,
CycloidGear,
BevelGear,
CrownGear,
WormGear,
TimingGear,
LanternGear,
HypoCycloidGear,
BaseGear,
)
from .timing_gear_t import TimingGearT
from .timinggear_t import TimingGearT
from .involutegear import InvoluteGear
from .internalinvolutegear import InternalInvoluteGear
from .involutegearrack import InvoluteGearRack
from .cycloidgearrack import CycloidGearRack
from .crowngear import CrownGear
from .cycloidgear import CycloidGear
from .bevelgear import BevelGear
from .wormgear import WormGear
from .timinggear import TimingGear
from .lanterngear import LanternGear
from .connector import GearConnector, ViewProviderGearConnector

View File

@@ -17,16 +17,16 @@
# ***************************************************************************
import os
import sys
import numpy as np
import FreeCAD
from pygears import __version__
from .features import (
InvoluteGear,
CycloidGear,
InvoluteGearRack,
CycloidGearRack,
InternalInvoluteGear,
)
from .involutegear import InvoluteGear
from .internalinvolutegear import InternalInvoluteGear
from .involutegearrack import InvoluteGearRack
from .cycloidgear import CycloidGear
from .cycloidgearrack import CycloidGearRack
from pygears.computation import compute_shifted_gears
@@ -43,6 +43,15 @@ class ViewProviderGearConnector(object):
def getIcon(self):
return self.icon_fn
if sys.version_info[0] == 3 and sys.version_info[1] >= 11:
def dumps(self):
return {"icon_fn": self.icon_fn}
def loads(self, state):
self.icon_fn = state["icon_fn"]
else:
def __getstate__(self):
return {"icon_fn": self.icon_fn}

143
freecad/gears/crowngear.py Normal file
View File

@@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import os
import sys
import FreeCAD as App
import Part
import numpy as np
from .basegear import BaseGear, fcvec
class CrownGear(BaseGear):
def __init__(self, obj):
super(CrownGear, self).__init__(obj)
obj.addProperty("App::PropertyInteger", "teeth", "base", "number of teeth")
obj.addProperty(
"App::PropertyInteger",
"other_teeth",
"base",
"number of teeth of other gear",
)
obj.addProperty("App::PropertyLength", "module", "base", "module")
obj.addProperty("App::PropertyLength", "height", "base", "height")
obj.addProperty("App::PropertyLength", "thickness", "base", "thickness")
obj.addProperty(
"App::PropertyAngle", "pressure_angle", "involute", "pressure angle"
)
self.add_accuracy_properties(obj)
obj.teeth = 15
obj.other_teeth = 15
obj.module = "1. mm"
obj.pressure_angle = "20. deg"
obj.height = "2. mm"
obj.thickness = "5 mm"
obj.num_profiles = 4
obj.preview_mode = True
self.obj = obj
obj.Proxy = self
App.Console.PrintMessage(
"Gear module: Crown gear created, preview_mode = true for improved performance. "
"Set preview_mode property to false when ready to cut teeth."
)
def add_accuracy_properties(self, obj):
obj.addProperty(
"App::PropertyInteger",
"num_profiles",
"accuracy",
"number of profiles used for loft",
)
obj.addProperty(
"App::PropertyBool",
"preview_mode",
"accuracy",
"if true no boolean operation is done",
)
def profile(self, m, r, r0, t_c, t_i, alpha_w, y0, y1, y2):
r_ew = m * t_i / 2
# 1: modifizierter Waelzkreisdurchmesser:
r_e = r / r0 * r_ew
# 2: modifizierter Schraegungswinkel:
alpha = np.arccos(r0 / r * np.cos(alpha_w))
# 3: winkel phi bei senkrechter stellung eines zahns:
phi = np.pi / t_i / 2 + (alpha - alpha_w) + (np.tan(alpha_w) - np.tan(alpha))
# 4: Position des Eingriffspunktes:
x_c = r_e * np.sin(phi)
dy = -r_e * np.cos(phi) + r_ew
# 5: oberer Punkt:
b = y1 - dy
a = np.tan(alpha) * b
x1 = a + x_c
# 6: unterer Punkt
d = y2 + dy
c = np.tan(alpha) * d
x2 = x_c - c
r *= np.cos(phi)
pts = [[-x1, r, y0], [-x2, r, y0 - y1 - y2], [x2, r, y0 - y1 - y2], [x1, r, y0]]
pts.append(pts[0])
return pts
def generate_gear_shape(self, fp):
inner_diameter = fp.module.Value * fp.teeth
outer_diameter = inner_diameter + fp.height.Value * 2
inner_circle = Part.Wire(Part.makeCircle(inner_diameter / 2.0))
outer_circle = Part.Wire(Part.makeCircle(outer_diameter / 2.0))
inner_circle.reverse()
face = Part.Face([outer_circle, inner_circle])
solid = face.extrude(App.Vector([0.0, 0.0, -fp.thickness.Value]))
if fp.preview_mode:
return solid
# cutting obj
alpha_w = np.deg2rad(fp.pressure_angle.Value)
m = fp.module.Value
t = fp.teeth
t_c = t
t_i = fp.other_teeth
rm = inner_diameter / 2
y0 = m * 0.5
y1 = m + y0
y2 = m
r0 = inner_diameter / 2 - fp.height.Value * 0.1
r1 = outer_diameter / 2 + fp.height.Value * 0.3
polies = []
for r_i in np.linspace(r0, r1, fp.num_profiles):
pts = self.profile(m, r_i, rm, t_c, t_i, alpha_w, y0, y1, y2)
poly = Part.Wire(Part.makePolygon(list(map(fcvec, pts))))
polies.append(poly)
loft = Part.makeLoft(polies, True)
rot = App.Matrix()
rot.rotateZ(2 * np.pi / t)
cut_shapes = []
for _ in range(t):
loft = loft.transformGeometry(rot)
cut_shapes.append(loft)
return solid.cut(cut_shapes)

View File

@@ -0,0 +1,193 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import FreeCAD as App
import Part
import numpy as np
from pygears.cycloid_tooth import CycloidTooth
from pygears._functions import rotation
from .basegear import (
BaseGear,
points_to_wire,
insert_fillet,
helicalextrusion,
rotate_tooth,
)
class CycloidGear(BaseGear):
"""FreeCAD gear"""
def __init__(self, obj):
super(CycloidGear, self).__init__(obj)
self.cycloid_tooth = CycloidTooth()
obj.addProperty("App::PropertyInteger", "teeth", "base", "number of teeth")
obj.addProperty("App::PropertyLength", "module", "base", "module")
obj.addProperty("App::PropertyLength", "height", "base", "height")
obj.addProperty(
"App::PropertyInteger",
"numpoints",
"accuracy",
"number of points for spline",
)
obj.addProperty(
"App::PropertyPythonObject", "gear", "base", "the python object"
)
self.add_helical_properties(obj)
self.add_fillet_properties(obj)
self.add_tolerance_properties(obj)
self.add_cycloid_properties(obj)
self.add_computed_properties(obj)
obj.gear = self.cycloid_tooth
obj.teeth = 15
obj.module = "1. mm"
obj.setExpression(
"inner_diameter", "teeth / 2"
) # teeth/2 makes the hypocycloid a straight line to the center
obj.outer_diameter = 7.5 # we don't know the mating gear, so we just set the default to mesh with our default
obj.beta = "0. deg"
obj.height = "5. mm"
obj.clearance = 0.25
obj.numpoints = 15
obj.backlash = "0.00 mm"
obj.double_helix = False
obj.head = 0
obj.head_fillet = 0
obj.root_fillet = 0
obj.Proxy = self
def add_helical_properties(self, obj):
obj.addProperty("App::PropertyBool", "double_helix", "helical", "double helix")
obj.addProperty("App::PropertyAngle", "beta", "helical", "beta")
def add_fillet_properties(self, obj):
obj.addProperty(
"App::PropertyFloat",
"head_fillet",
"fillets",
"a fillet for the tooth-head, radius = head_fillet x module",
)
obj.addProperty(
"App::PropertyFloat",
"root_fillet",
"fillets",
"a fillet for the tooth-root, radius = root_fillet x module",
)
def add_tolerance_properties(self, obj):
obj.addProperty("App::PropertyFloat", "clearance", "tolerance", "clearance")
obj.addProperty(
"App::PropertyLength",
"backlash",
"tolerance",
"The arc length on the pitch circle by which the tooth thicknes is reduced.",
)
obj.addProperty(
"App::PropertyFloat",
"head",
"tolerance",
"head_value * modul_value = additional length of head",
)
def add_cycloid_properties(self, obj):
obj.addProperty(
"App::PropertyFloat",
"inner_diameter",
"cycloid",
"inner_diameter divided by module (hypocycloid)",
)
obj.addProperty(
"App::PropertyFloat",
"outer_diameter",
"cycloid",
"outer_diameter divided by module (epicycloid)",
)
def add_computed_properties(self, obj):
obj.addProperty("App::PropertyLength", "dw", "computed", "The pitch diameter.")
obj.setExpression(
"dw", "teeth * module"
) # calculate via expression to ease usage for placement
obj.setEditorMode(
"dw", 1
) # set read-only after setting the expression, else it won't be visible. bug?
obj.addProperty(
"App::PropertyAngle",
"angular_backlash",
"computed",
"The angle by which this gear can turn without moving the mating gear.",
)
obj.setExpression(
"angular_backlash", "backlash / dw * 360° / pi"
) # calculate via expression to ease usage for placement
obj.setEditorMode(
"angular_backlash", 1
) # set read-only after setting the expression, else it won't be visible. bug?
def generate_gear_shape(self, fp):
fp.gear.m = fp.module.Value
fp.gear.z = fp.teeth
fp.dw = fp.module * fp.teeth
fp.gear.z1 = fp.inner_diameter
fp.gear.z2 = fp.outer_diameter
fp.gear.clearance = fp.clearance
fp.gear.head = fp.head
fp.gear.backlash = fp.backlash.Value
fp.gear._update()
pts = fp.gear.points(num=fp.numpoints)
rot = rotation(-fp.gear.phipart)
rotated_pts = list(map(rot, pts))
pts.append([pts[-1][-1], rotated_pts[0][0]])
pts += rotated_pts
tooth = points_to_wire(pts)
edges = tooth.Edges
r_head = float(fp.head_fillet * fp.module)
r_root = float(fp.root_fillet * fp.module)
pos_head = [0, 2, 6]
pos_root = [4, 6]
edge_range = [1, 9]
for pos in pos_head:
edges = insert_fillet(edges, pos, r_head)
for pos in pos_root:
edges = insert_fillet(edges, pos, r_root)
edges = edges[edge_range[0] : edge_range[1]]
edges = [e for e in edges if e is not None]
tooth = Part.Wire(edges)
profile = rotate_tooth(tooth, fp.teeth)
if fp.height.Value == 0:
return profile
base = Part.Face(profile)
if fp.beta.Value == 0:
return base.extrude(App.Vector(0, 0, fp.height.Value))
else:
twist_angle = (
fp.height.Value * np.tan(fp.beta.Value * np.pi / 180) * 2 / fp.gear.d
)
return helicalextrusion(base, fp.height.Value, twist_angle, fp.double_helix)

View File

@@ -0,0 +1,232 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import os
import sys
import FreeCAD as App
import Part
import numpy as np
from pygears._functions import reflection
from .basegear import BaseGear, fcvec, points_to_wire, insert_fillet
class CycloidGearRack(BaseGear):
"""FreeCAD gear rack"""
def __init__(self, obj):
super(CycloidGearRack, self).__init__(obj)
obj.addProperty("App::PropertyInteger", "teeth", "base", "number of teeth")
obj.addProperty("App::PropertyLength", "height", "base", "height")
obj.addProperty("App::PropertyLength", "thickness", "base", "thickness")
obj.addProperty("App::PropertyLength", "module", "involute", "module")
obj.addProperty(
"App::PropertyBool",
"simplified",
"precision",
"if enabled the rack is drawn with a constant number of \
teeth to avoid topologic renaming.",
)
obj.addProperty(
"App::PropertyInteger",
"numpoints",
"accuracy",
"number of points for spline",
)
obj.addProperty("App::PropertyPythonObject", "rack", "base", "test")
self.add_helical_properties(obj)
self.add_computed_properties(obj)
self.add_tolerance_properties(obj)
self.add_cycloid_properties(obj)
self.add_fillet_properties(obj)
obj.teeth = 15
obj.module = "1. mm"
obj.inner_diameter = 7.5
obj.outer_diameter = 7.5
obj.height = "5. mm"
obj.thickness = "5 mm"
obj.beta = "0. deg"
obj.clearance = 0.25
obj.head = 0.0
obj.add_endings = True
obj.simplified = False
obj.numpoints = 15
self.obj = obj
obj.Proxy = self
def add_helical_properties(self, obj):
obj.addProperty("App::PropertyAngle", "beta", "helical", "beta ")
obj.addProperty("App::PropertyBool", "double_helix", "helical", "double helix")
def add_computed_properties(self, obj):
obj.addProperty(
"App::PropertyLength",
"transverse_pitch",
"computed",
"pitch in the transverse plane",
1,
)
obj.addProperty(
"App::PropertyBool",
"add_endings",
"base",
"if enabled the total length of the rack is teeth x pitch, \
otherwise the rack starts with a tooth-flank",
)
def add_tolerance_properties(self, obj):
obj.addProperty(
"App::PropertyFloat",
"head",
"tolerance",
"head * module = additional length of head",
)
obj.addProperty(
"App::PropertyFloat",
"clearance",
"tolerance",
"clearance * module = additional length of root",
)
def add_cycloid_properties(self, obj):
obj.addProperty(
"App::PropertyFloat",
"inner_diameter",
"cycloid",
"inner_diameter divided by module (hypocycloid)",
)
obj.addProperty(
"App::PropertyFloat",
"outer_diameter",
"cycloid",
"outer_diameter divided by module (epicycloid)",
)
def add_fillet_properties(self, obj):
obj.addProperty(
"App::PropertyFloat",
"head_fillet",
"fillets",
"a fillet for the tooth-head, radius = head_fillet x module",
)
obj.addProperty(
"App::PropertyFloat",
"root_fillet",
"fillets",
"a fillet for the tooth-root, radius = root_fillet x module",
)
def generate_gear_shape(self, obj):
numpoints = obj.numpoints
m = obj.module.Value
t = obj.thickness.Value
r_i = obj.inner_diameter / 2 * m
r_o = obj.outer_diameter / 2 * m
c = obj.clearance
h = obj.head
head_fillet = obj.head_fillet
root_fillet = obj.root_fillet
phi_i_end = np.arccos(1 - m / r_i * (1 + c))
phi_o_end = np.arccos(1 - m / r_o * (1 + h))
phi_i = np.linspace(phi_i_end, 0, numpoints)
phi_o = np.linspace(0, phi_o_end, numpoints)
y_i = r_i * (np.cos(phi_i) - 1)
y_o = r_o * (1 - np.cos(phi_o))
x_i = r_i * (np.sin(phi_i) - phi_i) - m * np.pi / 4
x_o = r_o * (phi_o - np.sin(phi_o)) - m * np.pi / 4
x = x_i.tolist()[:-1] + x_o.tolist()
y = y_i.tolist()[:-1] + y_o.tolist()
points = np.array([y, x]).T
mirror = reflection(0)
points_1 = mirror(points)[::-1]
line_1 = [points[-1], points_1[0]]
line_2 = [points_1[-1], np.array([-(1 + c) * m, m * np.pi / 2])]
line_0 = [np.array([-(1 + c) * m, -m * np.pi / 2]), points[0]]
tooth = points_to_wire([line_0, points, line_1, points_1, line_2])
edges = tooth.Edges
edges = insert_fillet(edges, 0, m * root_fillet)
edges = insert_fillet(edges, 2, m * head_fillet)
edges = insert_fillet(edges, 4, m * head_fillet)
edges = insert_fillet(edges, 6, m * root_fillet)
tooth_edges = [e for e in edges if e is not None]
p_end = np.array(tooth_edges[-2].lastVertex().Point[:-1])
p_start = np.array(tooth_edges[1].firstVertex().Point[:-1])
p_start += np.array([0, np.pi * m])
edge = points_to_wire([[p_end, p_start]]).Edges
tooth = Part.Wire(tooth_edges[1:-1] + edge)
teeth = [tooth]
for i in range(obj.teeth - 1):
tooth = tooth.copy()
tooth.translate(App.Vector(0, np.pi * m, 0))
teeth.append(tooth)
teeth[-1] = Part.Wire(teeth[-1].Edges[:-1])
if obj.add_endings:
teeth = [Part.Wire(tooth_edges[0])] + teeth
last_edge = tooth_edges[-1]
last_edge.translate(App.Vector(0, np.pi * m * (obj.teeth - 1), 0))
teeth = teeth + [Part.Wire(last_edge)]
p_start = np.array(teeth[0].Edges[0].firstVertex().Point[:-1])
p_end = np.array(teeth[-1].Edges[-1].lastVertex().Point[:-1])
p_start_1 = p_start - np.array([obj.thickness.Value, 0.0])
p_end_1 = p_end - np.array([obj.thickness.Value, 0.0])
line6 = [p_start, p_start_1]
line7 = [p_start_1, p_end_1]
line8 = [p_end_1, p_end]
bottom = points_to_wire([line6, line7, line8])
pol = Part.Wire([bottom] + teeth)
if obj.height.Value == 0:
return pol
elif obj.beta.Value == 0:
face = Part.Face(Part.Wire(pol))
return face.extrude(fcvec([0.0, 0.0, obj.height.Value]))
elif obj.double_helix:
beta = obj.beta.Value * np.pi / 180.0
pol2 = Part.Wire(pol)
pol2.translate(
fcvec([0.0, np.tan(beta) * obj.height.Value / 2, obj.height.Value / 2])
)
pol3 = Part.Wire(pol)
pol3.translate(fcvec([0.0, 0.0, obj.height.Value]))
return Part.makeLoft([pol, pol2, pol3], True, True)
else:
beta = obj.beta.Value * np.pi / 180.0
pol2 = Part.Wire(pol)
pol2.translate(
fcvec([0.0, np.tan(beta) * obj.height.Value, obj.height.Value])
)
return Part.makeLoft([pol, pol2], True)
def __getstate__(self):
return None
def __setstate__(self, state):
return None

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,283 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import math
import numpy as np
import scipy as sp
import FreeCAD as App
import Part
from pygears.bevel_tooth import BevelTooth
from pygears._functions import rotation
from .basegear import BaseGear, make_bspline_wire
class HypoCycloidGear(BaseGear):
"""parameters:
pressure_angle: pressureangle, 10-30°
pitch_angle: cone angle, 0 < pitch_angle < pi/4
"""
def __init__(self, obj):
super(HypoCycloidGear, self).__init__(obj)
obj.addProperty(
"App::PropertyFloat",
"pin_circle_radius",
"gear_parameter",
"Pin ball circle radius(overrides Tooth Pitch",
)
obj.addProperty(
"App::PropertyFloat", "roller_diameter", "gear_parameter", "Roller Diameter"
)
obj.addProperty(
"App::PropertyFloat", "eccentricity", "gear_parameter", "Eccentricity"
)
obj.addProperty(
"App::PropertyAngle",
"pressure_angle_lim",
"gear_parameter",
"Pressure angle limit",
)
obj.addProperty(
"App::PropertyFloat",
"pressure_angle_offset",
"gear_parameter",
"Offset in pressure angle",
)
obj.addProperty(
"App::PropertyInteger",
"teeth_number",
"gear_parameter",
"Number of teeth in Cam",
)
obj.addProperty(
"App::PropertyInteger",
"segment_count",
"gear_parameter",
"Number of points used for spline interpolation",
)
obj.addProperty(
"App::PropertyLength",
"hole_radius",
"gear_parameter",
"Center hole's radius",
)
obj.addProperty(
"App::PropertyBool", "show_pins", "Pins", "Create pins in place"
)
obj.addProperty("App::PropertyLength", "pin_height", "Pins", "height")
obj.addProperty(
"App::PropertyBool",
"center_pins",
"Pins",
"Center pin Z axis to generated disks",
)
obj.addProperty(
"App::PropertyBool", "show_disk0", "Disks", "Show main cam disk"
)
obj.addProperty(
"App::PropertyBool",
"show_disk1",
"Disks",
"Show another reversed cam disk on top",
)
obj.addProperty("App::PropertyLength", "disk_height", "Disks", "height")
obj.pin_circle_radius = 66
obj.roller_diameter = 3
obj.eccentricity = 1.5
obj.pressure_angle_lim = "50.0 deg"
obj.pressure_angle_offset = 0.01
obj.teeth_number = 42
obj.segment_count = 42
obj.hole_radius = "30. mm"
obj.show_pins = True
obj.pin_height = "20. mm"
obj.center_pins = True
obj.show_disk0 = True
obj.show_disk1 = True
obj.disk_height = "10. mm"
self.obj = obj
obj.Proxy = self
def to_polar(self, x, y):
return (x**2 + y**2) ** 0.5, math.atan2(y, x)
def to_rect(self, r, a):
return r * math.cos(a), r * math.sin(a)
def calcyp(self, p, a, e, n):
return math.atan(math.sin(n * a) / (math.cos(n * a) + (n * p) / (e * (n + 1))))
def calc_x(self, p, d, e, n, a):
return (
(n * p) * math.cos(a)
+ e * math.cos((n + 1) * a)
- d / 2 * math.cos(self.calcyp(p, a, e, n) + a)
)
def calc_y(self, p, d, e, n, a):
return (
(n * p) * math.sin(a)
+ e * math.sin((n + 1) * a)
- d / 2 * math.sin(self.calcyp(p, a, e, n) + a)
)
def calc_pressure_angle(self, p, d, n, a):
ex = 2**0.5
r3 = p * n
rg = r3 / ex
pp = rg * (ex**2 + 1 - 2 * ex * math.cos(a)) ** 0.5 - d / 2
return math.asin((r3 * math.cos(a) - rg) / (pp + d / 2)) * 180 / math.pi
def calc_pressure_limit(self, p, d, e, n, a):
ex = 2**0.5
r3 = p * n
rg = r3 / ex
q = (r3**2 + rg**2 - 2 * r3 * rg * math.cos(a)) ** 0.5
x = rg - e + (q - d / 2) * (r3 * math.cos(a) - rg) / q
y = (q - d / 2) * r3 * math.sin(a) / q
return (x**2 + y**2) ** 0.5
def check_limit(self, x, y, maxrad, minrad, offset):
r, a = self.to_polar(x, y)
if (r > maxrad) or (r < minrad):
r = r - offset
x, y = self.to_rect(r, a)
return x, y
def generate_gear_shape(self, fp):
b = fp.pin_circle_radius
d = fp.roller_diameter
e = fp.eccentricity
n = fp.teeth_number
p = b / n
s = fp.segment_count
ang = fp.pressure_angle_lim
c = fp.pressure_angle_offset
q = 2 * math.pi / float(s)
# Find the pressure angle limit circles
minAngle = -1.0
maxAngle = -1.0
for i in range(0, 180):
x = self.calc_pressure_angle(p, d, n, i * math.pi / 180.0)
if (x < ang) and (minAngle < 0):
minAngle = float(i)
if (x < -ang) and (maxAngle < 0):
maxAngle = float(i - 1)
minRadius = self.calc_pressure_limit(p, d, e, n, minAngle * math.pi / 180.0)
maxRadius = self.calc_pressure_limit(p, d, e, n, maxAngle * math.pi / 180.0)
# unused
# Part.Wire(Part.makeCircle(minRadius,App.Vector(-e, 0, 0)))
# Part.Wire(Part.makeCircle(maxRadius,App.Vector(-e, 0, 0)))
App.Console.PrintMessage("Generating cam disk\r\n")
# generate the cam profile - note: shifted in -x by eccentricicy amount
i = 0
x = self.calc_x(p, d, e, n, q * i / float(n))
y = self.calc_y(p, d, e, n, q * i / n)
x, y = self.check_limit(x, y, maxRadius, minRadius, c)
points = [App.Vector(x - e, y, 0)]
for i in range(0, s):
x = self.calc_x(p, d, e, n, q * (i + 1) / n)
y = self.calc_y(p, d, e, n, q * (i + 1) / n)
x, y = self.check_limit(x, y, maxRadius, minRadius, c)
points.append([x - e, y, 0])
wi = make_bspline_wire([points])
wires = []
mat = App.Matrix()
mat.move(App.Vector(e, 0.0, 0.0))
mat.rotateZ(2 * np.pi / n)
mat.move(App.Vector(-e, 0.0, 0.0))
for _ in range(n):
wi = wi.transformGeometry(mat)
wires.append(wi)
cam = Part.Face(Part.Wire(wires))
# add a circle in the center of the cam
if fp.hole_radius.Value:
centerCircle = Part.Face(
Part.Wire(Part.makeCircle(fp.hole_radius.Value, App.Vector(-e, 0, 0)))
)
cam = cam.cut(centerCircle)
to_be_fused = []
if fp.show_disk0 == True:
if fp.disk_height.Value == 0:
to_be_fused.append(cam)
else:
to_be_fused.append(cam.extrude(App.Vector(0, 0, fp.disk_height.Value)))
# secondary cam disk
if fp.show_disk1 == True:
App.Console.PrintMessage("Generating secondary cam disk\r\n")
second_cam = cam.copy()
mat = App.Matrix()
mat.rotateZ(np.pi)
mat.move(App.Vector(-e, 0, 0))
if n % 2 == 0:
mat.rotateZ(np.pi / n)
mat.move(App.Vector(e, 0, 0))
second_cam = second_cam.transformGeometry(mat)
if fp.disk_height.Value == 0:
to_be_fused.append(second_cam)
else:
to_be_fused.append(
second_cam.extrude(App.Vector(0, 0, -fp.disk_height.Value))
)
# pins
if fp.show_pins == True:
App.Console.PrintMessage("Generating pins\r\n")
pins = []
for i in range(0, n + 1):
x = p * n * math.cos(2 * math.pi / (n + 1) * i)
y = p * n * math.sin(2 * math.pi / (n + 1) * i)
pins.append(Part.Wire(Part.makeCircle(d / 2, App.Vector(x, y, 0))))
pins = Part.Face(pins)
z_offset = -fp.pin_height.Value / 2
if fp.center_pins == True:
if fp.show_disk0 == True and fp.show_disk1 == False:
z_offset += fp.disk_height.Value / 2
elif fp.show_disk0 == False and fp.show_disk1 == True:
z_offset += -fp.disk_height.Value / 2
# extrude
if z_offset != 0:
pins.translate(App.Vector(0, 0, z_offset))
if fp.pin_height != 0:
pins = pins.extrude(App.Vector(0, 0, fp.pin_height.Value))
to_be_fused.append(pins)
if to_be_fused:
return Part.makeCompound(to_be_fused)

View File

@@ -90,6 +90,7 @@ if sys.version_info[0] == 3 and sys.version_info[1] >= 11:
)
)
class GearWorkbench(Workbench):
"""A freecad workbench aiming at gear design"""
@@ -129,7 +130,7 @@ class GearWorkbench(Workbench):
CreateLanternGear,
CreateHypoCycloidGear,
CreateCycloidRack,
CreateGearConnector
CreateGearConnector,
)
self.appendToolbar("Gear", self.commands)

View File

@@ -0,0 +1,248 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import FreeCAD as App
import Part
import numpy as np
from pygears.involute_tooth import InvoluteTooth
from pygears._functions import rotation
from .basegear import (
BaseGear,
points_to_wire,
insert_fillet,
helicalextrusion,
rotate_tooth,
)
class InternalInvoluteGear(BaseGear):
"""FreeCAD internal involute gear
Using the same tooth as the external, just turning it inside-out:
addedum becomes dedendum, clearance becomes head, negate the backslash, ...
"""
def __init__(self, obj):
super(InternalInvoluteGear, self).__init__(obj)
self.involute_tooth = InvoluteTooth()
obj.addProperty("App::PropertyBool", "simple", "precision", "simple")
obj.addProperty("App::PropertyInteger", "teeth", "base", "number of teeth")
obj.addProperty(
"App::PropertyLength",
"module",
"base",
"normal module if properties_from_tool=True, \
else it's the transverse module.",
)
obj.addProperty("App::PropertyLength", "height", "base", "height")
obj.addProperty("App::PropertyLength", "thickness", "base", "thickness")
obj.addProperty(
"App::PropertyInteger",
"numpoints",
"accuracy",
"number of points for spline",
)
obj.addProperty("App::PropertyPythonObject", "gear", "base", "test")
self.add_involute_properties(obj)
self.add_tolerance_properties(obj)
self.add_fillet_properties(obj)
self.add_computed_properties(obj)
self.add_limiting_diameter_properties(obj)
self.add_helical_properties(obj)
obj.gear = self.involute_tooth
obj.simple = False
obj.teeth = 15
obj.module = "1. mm"
obj.shift = 0.0
obj.pressure_angle = "20. deg"
obj.beta = "0. deg"
obj.height = "5. mm"
obj.thickness = "5 mm"
obj.clearance = 0.25
obj.head = -0.4 # using head=0 and shift=0.5 may be better, but makes placeing the pinion less intuitive
obj.numpoints = 6
obj.double_helix = False
obj.backlash = "0.00 mm"
obj.reversed_backlash = False
obj.properties_from_tool = False
obj.head_fillet = 0
obj.root_fillet = 0
self.obj = obj
obj.Proxy = self
def add_limiting_diameter_properties(self, obj):
obj.addProperty("App::PropertyLength", "da", "computed", "inside diameter", 1)
obj.addProperty("App::PropertyLength", "df", "computed", "root diameter", 1)
def add_computed_properties(self, obj):
obj.addProperty("App::PropertyLength", "dw", "computed", "The pitch diameter.")
obj.addProperty(
"App::PropertyAngle",
"angular_backlash",
"computed",
"The angle by which this gear can turn without moving the mating gear.",
)
obj.setExpression(
"angular_backlash", "backlash / dw * 360° / pi"
) # calculate via expression to ease usage for placement
obj.setEditorMode(
"angular_backlash", 1
) # set read-only after setting the expression, else it won't be visible. bug?
obj.addProperty(
"App::PropertyLength", "transverse_pitch", "computed", "transverse_pitch", 1
)
obj.addProperty(
"App::PropertyLength", "outside_diameter", "computed", "Outside diameter", 1
)
def add_fillet_properties(self, obj):
obj.addProperty(
"App::PropertyFloat",
"head_fillet",
"fillets",
"a fillet for the tooth-head, radius = head_fillet x module",
)
obj.addProperty(
"App::PropertyFloat",
"root_fillet",
"fillets",
"a fillet for the tooth-root, radius = root_fillet x module",
)
def add_tolerance_properties(self, obj):
obj.addProperty(
"App::PropertyLength",
"backlash",
"tolerance",
"The arc length on the pitch circle by which the tooth thicknes is reduced.",
)
obj.addProperty(
"App::PropertyBool", "reversed_backlash", "tolerance", "backlash direction"
)
obj.addProperty(
"App::PropertyFloat",
"head",
"tolerance",
"head_value * modul_value = additional length of head",
)
obj.addProperty("App::PropertyFloat", "clearance", "tolerance", "clearance")
def add_involute_properties(self, obj):
obj.addProperty("App::PropertyFloat", "shift", "involute", "shift")
obj.addProperty(
"App::PropertyAngle", "pressure_angle", "involute", "pressure angle"
)
def add_helical_properties(self, obj):
obj.addProperty("App::PropertyAngle", "beta", "helical", "beta ")
obj.addProperty("App::PropertyBool", "double_helix", "helical", "double helix")
obj.addProperty(
"App::PropertyBool",
"properties_from_tool",
"helical",
"if beta is given and properties_from_tool is enabled, \
gear parameters are internally recomputed for the rotated gear",
)
def generate_gear_shape(self, fp):
fp.gear.double_helix = fp.double_helix
fp.gear.m_n = fp.module.Value
fp.gear.z = fp.teeth
fp.gear.undercut = False # no undercut for internal gears
fp.gear.shift = fp.shift
fp.gear.pressure_angle = fp.pressure_angle.Value * np.pi / 180.0
fp.gear.beta = fp.beta.Value * np.pi / 180
fp.gear.clearance = fp.head # swap head and clearance to become "internal"
fp.gear.backlash = (
fp.backlash.Value * (fp.reversed_backlash - 0.5) * 2.0
) # negate "reversed_backslash", for "internal"
fp.gear.head = fp.clearance # swap head and clearance to become "internal"
fp.gear.properties_from_tool = fp.properties_from_tool
fp.gear._update()
fp.dw = "{}mm".format(fp.gear.dw)
# computed properties
fp.transverse_pitch = "{}mm".format(fp.gear.pitch)
fp.outside_diameter = fp.dw + 2 * fp.thickness
# checksbackwardcompatibility:
if not "da" in fp.PropertiesList:
self.add_limiting_diameter_properties(fp)
fp.da = "{}mm".format(fp.gear.df) # swap addednum and dedendum for "internal"
fp.df = "{}mm".format(fp.gear.da) # swap addednum and dedendum for "internal"
outer_circle = Part.Wire(Part.makeCircle(fp.outside_diameter / 2.0))
outer_circle.reverse()
if not fp.simple:
# head-fillet:
pts = fp.gear.points(num=fp.numpoints)
rot = rotation(-fp.gear.phipart)
rotated_pts = list(map(rot, pts))
pts.append([pts[-1][-1], rotated_pts[0][0]])
pts += rotated_pts
tooth = points_to_wire(pts)
r_head = float(fp.root_fillet * fp.module) # reversing head
r_root = float(fp.head_fillet * fp.module) # and foot
edges = tooth.Edges
if len(tooth.Edges) == 11:
pos_head = [1, 3, 9]
pos_root = [6, 8]
edge_range = [2, 12]
else:
pos_head = [0, 2, 6]
pos_root = [4, 6]
edge_range = [1, 9]
for pos in pos_head:
edges = insert_fillet(edges, pos, r_head)
for pos in pos_root:
try:
edges = insert_fillet(edges, pos, r_root)
except RuntimeError:
edges.pop(8)
edges.pop(6)
edge_range = [2, 10]
pos_root = [5, 7]
for pos in pos_root:
edges = insert_fillet(edges, pos, r_root)
break
edges = edges[edge_range[0] : edge_range[1]]
edges = [e for e in edges if e is not None]
tooth = Part.Wire(edges)
profile = rotate_tooth(tooth, fp.teeth)
if fp.height.Value == 0:
return Part.makeCompound([outer_circle, profile])
base = Part.Face([outer_circle, profile])
if fp.beta.Value == 0:
return base.extrude(App.Vector(0, 0, fp.height.Value))
else:
twist_angle = fp.height.Value * np.tan(fp.gear.beta) * 2 / fp.gear.d
return helicalextrusion(
base, fp.height.Value, twist_angle, fp.double_helix
)
else:
inner_circle = Part.Wire(Part.makeCircle(fp.dw / 2.0))
inner_circle.reverse()
base = Part.Face([outer_circle, inner_circle])
return base.extrude(App.Vector(0, 0, fp.height.Value))

View File

@@ -0,0 +1,264 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import FreeCAD as App
import Part
import numpy as np
from pygears.involute_tooth import InvoluteTooth
from pygears._functions import rotation
from .basegear import (
BaseGear,
points_to_wire,
insert_fillet,
helicalextrusion,
rotate_tooth,
)
class InvoluteGear(BaseGear):
"""FreeCAD gear"""
def __init__(self, obj):
super(InvoluteGear, self).__init__(obj)
self.involute_tooth = InvoluteTooth()
obj.addProperty(
"App::PropertyPythonObject", "gear", "base", "python gear object"
)
self.add_gear_properties(obj)
self.add_fillet_properties(obj)
self.add_helical_properties(obj)
self.add_computed_properties(obj)
self.add_tolerance_properties(obj)
self.add_accuracy_properties(obj)
obj.gear = self.involute_tooth
obj.simple = False
obj.undercut = False
obj.teeth = 15
obj.module = "1. mm"
obj.shift = 0.0
obj.pressure_angle = "20. deg"
obj.beta = "0. deg"
obj.height = "5. mm"
obj.clearance = 0.25
obj.head = 0.0
obj.numpoints = 6
obj.double_helix = False
obj.backlash = "0.00 mm"
obj.reversed_backlash = False
obj.properties_from_tool = False
obj.head_fillet = 0
obj.root_fillet = 0
self.obj = obj
obj.Proxy = self
self.compute_traverse_properties(obj)
def add_gear_properties(self, obj):
obj.addProperty("App::PropertyInteger", "teeth", "base", "number of teeth")
obj.addProperty(
"App::PropertyLength",
"module",
"base",
"normal module if properties_from_tool=True, \
else it's the transverse module.",
)
obj.addProperty("App::PropertyLength", "height", "base", "height")
obj.addProperty(
"App::PropertyAngle", "pressure_angle", "involute", "pressure angle"
)
obj.addProperty("App::PropertyFloat", "shift", "involute", "shift")
def add_fillet_properties(self, obj):
obj.addProperty("App::PropertyBool", "undercut", "fillets", "undercut")
obj.addProperty(
"App::PropertyFloat",
"head_fillet",
"fillets",
"a fillet for the tooth-head, radius = head_fillet x module",
)
obj.addProperty(
"App::PropertyFloat",
"root_fillet",
"fillets",
"a fillet for the tooth-root, radius = root_fillet x module",
)
def add_helical_properties(self, obj):
obj.addProperty(
"App::PropertyBool",
"properties_from_tool",
"helical",
"if beta is given and properties_from_tool is enabled, \
gear parameters are internally recomputed for the rotated gear",
)
obj.addProperty("App::PropertyAngle", "beta", "helical", "beta ")
obj.addProperty("App::PropertyBool", "double_helix", "helical", "double helix")
def add_computed_properties(self, obj):
obj.addProperty("App::PropertyLength", "da", "computed", "outside diameter", 1)
obj.addProperty("App::PropertyLength", "df", "computed", "root diameter", 1)
self.add_traverse_module_property(obj)
obj.addProperty(
"App::PropertyLength", "dw", "computed", "The pitch diameter.", 1
)
obj.addProperty(
"App::PropertyAngle",
"angular_backlash",
"computed",
"The angle by which this gear can turn without moving the mating gear.",
)
obj.setExpression(
"angular_backlash", "backlash / dw * 360° / pi"
) # calculate via expression to ease usage for placement
obj.setEditorMode(
"angular_backlash", 1
) # set read-only after setting the expression, else it won't be visible. bug?
obj.addProperty(
"App::PropertyLength", "transverse_pitch", "computed", "transverse_pitch", 1
)
def add_tolerance_properties(self, obj):
obj.addProperty(
"App::PropertyLength",
"backlash",
"tolerance",
"The arc length on the pitch circle by which the tooth thicknes is reduced.",
)
obj.addProperty(
"App::PropertyBool", "reversed_backlash", "tolerance", "backlash direction"
)
obj.addProperty("App::PropertyFloat", "clearance", "tolerance", "clearance")
obj.addProperty(
"App::PropertyFloat",
"head",
"tolerance",
"head_value * modul_value = additional length of head",
)
def add_accuracy_properties(self, obj):
obj.addProperty("App::PropertyBool", "simple", "accuracy", "simple")
obj.addProperty(
"App::PropertyInteger",
"numpoints",
"accuracy",
"number of points for spline",
)
def add_traverse_module_property(self, obj):
obj.addProperty(
"App::PropertyLength",
"traverse_module",
"computed",
"traverse module of the generated gear",
1,
)
def compute_traverse_properties(self, obj):
# traverse_module added recently, if old freecad doc is loaded without it, it will not exist when generate_gear_shape() is called
if not hasattr(obj, "traverse_module"):
self.add_traverse_module_property(obj)
if obj.properties_from_tool:
obj.traverse_module = obj.module / np.cos(obj.gear.beta)
else:
obj.traverse_module = obj.module
obj.transverse_pitch = "{}mm".format(obj.gear.pitch)
obj.da = "{}mm".format(obj.gear.da)
obj.df = "{}mm".format(obj.gear.df)
obj.dw = "{}mm".format(obj.gear.dw)
def generate_gear_shape(self, obj):
obj.gear.double_helix = obj.double_helix
obj.gear.m_n = obj.module.Value
obj.gear.z = obj.teeth
obj.gear.undercut = obj.undercut
obj.gear.shift = obj.shift
obj.gear.pressure_angle = obj.pressure_angle.Value * np.pi / 180.0
obj.gear.beta = obj.beta.Value * np.pi / 180
obj.gear.clearance = obj.clearance
obj.gear.backlash = obj.backlash.Value * (-obj.reversed_backlash + 0.5) * 2.0
obj.gear.head = obj.head
obj.gear.properties_from_tool = obj.properties_from_tool
obj.gear._update()
self.compute_traverse_properties(obj)
if not obj.simple:
pts = obj.gear.points(num=obj.numpoints)
rot = rotation(-obj.gear.phipart)
rotated_pts = list(map(rot, pts))
pts.append([pts[-1][-1], rotated_pts[0][0]])
pts += rotated_pts
tooth = points_to_wire(pts)
edges = tooth.Edges
# head-fillet:
r_head = float(obj.head_fillet * obj.module)
r_root = float(obj.root_fillet * obj.module)
if obj.undercut and r_root != 0.0:
r_root = 0.0
App.Console.PrintWarning(
"root fillet is not allowed if undercut is computed"
)
if len(tooth.Edges) == 11:
pos_head = [1, 3, 9]
pos_root = [6, 8]
edge_range = [2, 12]
else:
pos_head = [0, 2, 6]
pos_root = [4, 6]
edge_range = [1, 9]
for pos in pos_head:
edges = insert_fillet(edges, pos, r_head)
for pos in pos_root:
try:
edges = insert_fillet(edges, pos, r_root)
except RuntimeError:
edges.pop(8)
edges.pop(6)
edge_range = [2, 10]
pos_root = [5, 7]
for pos in pos_root:
edges = insert_fillet(edges, pos, r_root)
break
edges = edges[edge_range[0] : edge_range[1]]
edges = [e for e in edges if e is not None]
tooth = Part.Wire(edges)
profile = rotate_tooth(tooth, obj.teeth)
if obj.height.Value == 0:
return profile
base = Part.Face(profile)
if obj.beta.Value == 0:
return base.extrude(App.Vector(0, 0, obj.height.Value))
else:
twist_angle = obj.height.Value * np.tan(obj.gear.beta) * 2 / obj.gear.d
return helicalextrusion(
base, obj.height.Value, twist_angle, obj.double_helix
)
else:
rw = obj.gear.dw / 2
return Part.makeCylinder(rw, obj.height.Value)

View File

@@ -0,0 +1,239 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import FreeCAD as App
import Part
import numpy as np
from pygears.involute_tooth import InvoluteRack
from .basegear import BaseGear, fcvec, points_to_wire, insert_fillet
class InvoluteGearRack(BaseGear):
"""FreeCAD gear rack"""
def __init__(self, obj):
super(InvoluteGearRack, self).__init__(obj)
self.involute_rack = InvoluteRack()
obj.addProperty("App::PropertyInteger", "teeth", "base", "number of teeth")
obj.addProperty("App::PropertyLength", "height", "base", "height")
obj.addProperty("App::PropertyLength", "module", "base", "module")
obj.addProperty("App::PropertyLength", "thickness", "base", "thickness")
obj.addProperty(
"App::PropertyBool",
"simplified",
"precision",
"if enabled the rack is drawn with a constant number of \
teeth to avoid topologic renaming.",
)
obj.addProperty("App::PropertyPythonObject", "rack", "base", "test")
self.add_helical_properties(obj)
self.add_computed_properties(obj)
self.add_tolerance_properties(obj)
self.add_involute_properties(obj)
self.add_fillet_properties(obj)
obj.rack = self.involute_rack
obj.teeth = 15
obj.module = "1. mm"
obj.pressure_angle = "20. deg"
obj.height = "5. mm"
obj.thickness = "5 mm"
obj.beta = "0. deg"
obj.clearance = 0.25
obj.head = 0.0
obj.properties_from_tool = False
obj.add_endings = True
obj.simplified = False
self.obj = obj
obj.Proxy = self
def add_helical_properties(self, obj):
obj.addProperty(
"App::PropertyBool",
"properties_from_tool",
"helical",
"if beta is given and properties_from_tool is enabled, \
gear parameters are internally recomputed for the rotated gear",
)
obj.addProperty("App::PropertyAngle", "beta", "helical", "beta ")
obj.addProperty("App::PropertyBool", "double_helix", "helical", "double helix")
def add_computed_properties(self, obj):
obj.addProperty(
"App::PropertyLength",
"transverse_pitch",
"computed",
"pitch in the transverse plane",
1,
)
obj.addProperty(
"App::PropertyBool",
"add_endings",
"base",
"if enabled the total length of the rack is teeth x pitch, \
otherwise the rack starts with a tooth-flank",
)
def add_tolerance_properties(self, obj):
obj.addProperty(
"App::PropertyFloat",
"head",
"tolerance",
"head * module = additional length of head",
)
obj.addProperty(
"App::PropertyFloat",
"clearance",
"tolerance",
"clearance * module = additional length of root",
)
def add_involute_properties(self, obj):
obj.addProperty(
"App::PropertyAngle", "pressure_angle", "involute", "pressure angle"
)
def add_fillet_properties(self, obj):
obj.addProperty(
"App::PropertyFloat",
"head_fillet",
"fillets",
"a fillet for the tooth-head, radius = head_fillet x module",
)
obj.addProperty(
"App::PropertyFloat",
"root_fillet",
"fillets",
"a fillet for the tooth-root, radius = root_fillet x module",
)
def generate_gear_shape(self, obj):
obj.rack.m = obj.module.Value
obj.rack.z = obj.teeth
obj.rack.pressure_angle = obj.pressure_angle.Value * np.pi / 180.0
obj.rack.thickness = obj.thickness.Value
obj.rack.beta = obj.beta.Value * np.pi / 180.0
obj.rack.head = obj.head
# checksbackwardcompatibility:
if "clearance" in obj.PropertiesList:
obj.rack.clearance = obj.clearance
if "properties_from_tool" in obj.PropertiesList:
obj.rack.properties_from_tool = obj.properties_from_tool
if "add_endings" in obj.PropertiesList:
obj.rack.add_endings = obj.add_endings
if "simplified" in obj.PropertiesList:
obj.rack.simplified = obj.simplified
obj.rack._update()
m, m_n, pitch, pressure_angle_t = obj.rack.compute_properties()
obj.transverse_pitch = "{} mm".format(pitch)
t = obj.thickness.Value
c = obj.clearance
h = obj.head
alpha = obj.pressure_angle.Value * np.pi / 180.0
head_fillet = obj.head_fillet
root_fillet = obj.root_fillet
x1 = -m * np.pi / 2
y1 = -m * (1 + c)
y2 = y1
x2 = -m * np.pi / 4 + y2 * np.tan(alpha)
y3 = m * (1 + h)
x3 = -m * np.pi / 4 + y3 * np.tan(alpha)
x4 = -x3
x5 = -x2
x6 = -x1
y4 = y3
y5 = y2
y6 = y1
p1 = np.array([y1, x1])
p2 = np.array([y2, x2])
p3 = np.array([y3, x3])
p4 = np.array([y4, x4])
p5 = np.array([y5, x5])
p6 = np.array([y6, x6])
line1 = [p1, p2]
line2 = [p2, p3]
line3 = [p3, p4]
line4 = [p4, p5]
line5 = [p5, p6]
tooth = Part.Wire(points_to_wire([line1, line2, line3, line4, line5]))
edges = tooth.Edges
edges = insert_fillet(edges, 0, m * root_fillet)
edges = insert_fillet(edges, 2, m * head_fillet)
edges = insert_fillet(edges, 4, m * head_fillet)
edges = insert_fillet(edges, 6, m * root_fillet)
tooth_edges = [e for e in edges if e is not None]
p_end = np.array(tooth_edges[-2].lastVertex().Point[:-1])
p_start = np.array(tooth_edges[1].firstVertex().Point[:-1])
p_start += np.array([0, np.pi * m])
edge = points_to_wire([[p_end, p_start]]).Edges
tooth = Part.Wire(tooth_edges[1:-1] + edge)
teeth = [tooth]
for i in range(obj.teeth - 1):
tooth = tooth.copy()
tooth.translate(App.Vector(0, np.pi * m, 0))
teeth.append(tooth)
teeth[-1] = Part.Wire(teeth[-1].Edges[:-1])
if obj.add_endings:
teeth = [Part.Wire(tooth_edges[0])] + teeth
last_edge = tooth_edges[-1]
last_edge.translate(App.Vector(0, np.pi * m * (obj.teeth - 1), 0))
teeth = teeth + [Part.Wire(last_edge)]
p_start = np.array(teeth[0].Edges[0].firstVertex().Point[:-1])
p_end = np.array(teeth[-1].Edges[-1].lastVertex().Point[:-1])
p_start_1 = p_start - np.array([obj.thickness.Value, 0.0])
p_end_1 = p_end - np.array([obj.thickness.Value, 0.0])
line6 = [p_start, p_start_1]
line7 = [p_start_1, p_end_1]
line8 = [p_end_1, p_end]
bottom = points_to_wire([line6, line7, line8])
pol = Part.Wire([bottom] + teeth)
if obj.height.Value == 0:
return pol
elif obj.beta.Value == 0:
face = Part.Face(Part.Wire(pol))
return face.extrude(fcvec([0.0, 0.0, obj.height.Value]))
elif obj.double_helix:
beta = obj.beta.Value * np.pi / 180.0
pol2 = Part.Wire(pol)
pol2.translate(
fcvec([0.0, np.tan(beta) * obj.height.Value / 2, obj.height.Value / 2])
)
pol3 = Part.Wire(pol)
pol3.translate(fcvec([0.0, 0.0, obj.height.Value]))
return Part.makeLoft([pol, pol2, pol3], True, True)
else:
beta = obj.beta.Value * np.pi / 180.0
pol2 = Part.Wire(pol)
pol2.translate(
fcvec([0.0, np.tan(beta) * obj.height.Value, obj.height.Value])
)
return Part.makeLoft([pol, pol2], True)

View File

@@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import FreeCAD as App
import Part
import numpy as np
import scipy as sp
from pygears.bevel_tooth import BevelTooth
from pygears._functions import rotation
from .basegear import BaseGear, fcvec, part_arc_from_points_and_center
class LanternGear(BaseGear):
def __init__(self, obj):
super(LanternGear, self).__init__(obj)
obj.addProperty(
"App::PropertyInteger", "teeth", "gear_parameter", "number of teeth"
)
obj.addProperty("App::PropertyLength", "module", "base", "module")
obj.addProperty(
"App::PropertyLength",
"bolt_radius",
"base",
"the bolt radius of the rack/chain",
)
obj.addProperty("App::PropertyLength", "height", "base", "height")
obj.addProperty(
"App::PropertyInteger",
"num_profiles",
"accuracy",
"number of profiles used for loft",
)
obj.addProperty(
"App::PropertyFloat",
"head",
"tolerance",
"head * module = additional length of head",
)
obj.teeth = 15
obj.module = "1. mm"
obj.bolt_radius = "1 mm"
obj.height = "5. mm"
obj.num_profiles = 10
self.obj = obj
obj.Proxy = self
def generate_gear_shape(self, fp):
m = fp.module.Value
teeth = fp.teeth
r_r = fp.bolt_radius.Value
r_0 = m * teeth / 2
r_max = r_0 + r_r + fp.head * m
phi_max = (r_r + np.sqrt(r_max**2 - r_0**2)) / r_0
def find_phi_min(phi_min):
return r_0 * (
phi_min**2 * r_0
- 2 * phi_min * r_0 * np.sin(phi_min)
- 2 * phi_min * r_r
- 2 * r_0 * np.cos(phi_min)
+ 2 * r_0
+ 2 * r_r * np.sin(phi_min)
)
phi_min = sp.optimize.root(find_phi_min, (phi_max + r_r / r_0 * 4) / 5).x[
0
] # , r_r / r_0, phi_max)
# phi_min = 0 # r_r / r_0
phi = np.linspace(phi_min, phi_max, fp.num_profiles)
x = r_0 * (np.cos(phi) + phi * np.sin(phi)) - r_r * np.sin(phi)
y = r_0 * (np.sin(phi) - phi * np.cos(phi)) + r_r * np.cos(phi)
xy1 = np.array([x, y]).T
p_1 = xy1[0]
p_1_end = xy1[-1]
bsp_1 = Part.BSplineCurve()
bsp_1.interpolate(list(map(fcvec, xy1)))
w_1 = bsp_1.toShape()
xy2 = xy1 * np.array([1.0, -1.0])
p_2 = xy2[0]
p_2_end = xy2[-1]
bsp_2 = Part.BSplineCurve()
bsp_2.interpolate(list(map(fcvec, xy2)))
w_2 = bsp_2.toShape()
p_12 = np.array([r_0 - r_r, 0.0])
arc = Part.Arc(
App.Vector(*p_1, 0.0), App.Vector(*p_12, 0.0), App.Vector(*p_2, 0.0)
).toShape()
rot = rotation(-np.pi * 2 / teeth)
p_3 = rot(np.array([p_2_end]))[0]
# l = Part.LineSegment(fcvec(p_1_end), fcvec(p_3)).toShape()
l = part_arc_from_points_and_center(
p_1_end, p_3, np.array([0.0, 0.0])
).toShape()
w = Part.Wire([w_2, arc, w_1, l])
wires = [w]
rot = App.Matrix()
for _ in range(teeth - 1):
rot.rotateZ(np.pi * 2 / teeth)
wires.append(w.transformGeometry(rot))
wi = Part.Wire(wires)
if fp.height.Value == 0:
return wi
else:
return Part.Face(wi).extrude(App.Vector(0, 0, fp.height))

285
freecad/gears/timinggear.py Normal file
View File

@@ -0,0 +1,285 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import FreeCAD as App
import Part
import numpy as np
from pygears._functions import reflection
from .basegear import BaseGear, part_arc_from_points_and_center
class TimingGear(BaseGear):
"""FreeCAD gear rack"""
data = {
"gt2": {
"pitch": 2.0,
"u": 0.254,
"h": 0.75,
"H": 1.38,
"r0": 0.555,
"r1": 1.0,
"rs": 0.15,
"offset": 0.40,
},
"gt3": {
"pitch": 3.0,
"u": 0.381,
"h": 1.14,
"H": 2.40,
"r0": 0.85,
"r1": 1.52,
"rs": 0.25,
"offset": 0.61,
},
"gt5": {
"pitch": 5.0,
"u": 0.5715,
"h": 1.93,
"H": 3.81,
"r0": 1.44,
"r1": 2.57,
"rs": 0.416,
"offset": 1.03,
},
"gt8": {
"pitch": 8.0,
"u": 0.9144,
"h": 3.088,
"H": 6.096,
"r0": 2.304,
"r1": 4.112,
"rs": 0.6656,
"offset": 1.648,
},
"htd3": {
"pitch": 3.0,
"u": 0.381,
"h": 1.21,
"H": 2.40,
"r0": 0.89,
"r1": 0.89,
"rs": 0.26,
"offset": 0.0,
},
"htd5": {
"pitch": 5.0,
"u": 0.5715,
"h": 2.06,
"H": 3.80,
"r0": 1.49,
"r1": 1.49,
"rs": 0.43,
"offset": 0.0,
},
"htd8": {
"pitch": 8.0,
"u": 0.686,
"h": 3.45,
"H": 6.00,
"r0": 2.46,
"r1": 2.46,
"rs": 0.70,
"offset": 0.0,
},
}
def __init__(self, obj):
super(TimingGear, self).__init__(obj)
obj.addProperty("App::PropertyInteger", "teeth", "base", "number of teeth")
obj.addProperty(
"App::PropertyEnumeration", "type", "base", "type of timing-gear"
)
obj.addProperty("App::PropertyLength", "height", "base", "height")
obj.addProperty("App::PropertyLength", "pitch", "computed", "pitch of gear", 1)
obj.addProperty(
"App::PropertyLength", "h", "computed", "radial height of teeth", 1
)
obj.addProperty(
"App::PropertyLength",
"u",
"computed",
"radial difference between pitch diameter and head of gear",
1,
)
obj.addProperty(
"App::PropertyLength", "r0", "computed", "radius of first arc", 1
)
obj.addProperty(
"App::PropertyLength", "r1", "computed", "radius of second arc", 1
)
obj.addProperty(
"App::PropertyLength", "rs", "computed", "radius of third arc", 1
)
obj.addProperty(
"App::PropertyLength",
"offset",
"computed",
"x-offset of second arc-midpoint",
1,
)
obj.teeth = 15
obj.type = ["gt2", "gt3", "gt5", "gt8", "htd3", "htd5", "htd8"]
obj.height = "5. mm"
self.obj = obj
obj.Proxy = self
def generate_gear_shape(self, fp):
# m ... center of arc/circle
# r ... radius of arc/circle
# x ... end-point of arc
# phi ... angle
tp = fp.type
gt_data = self.data[tp]
pitch = fp.pitch = gt_data["pitch"]
h = fp.h = gt_data["h"]
u = fp.u = gt_data["u"]
r_12 = fp.r0 = gt_data["r0"]
r_23 = fp.r1 = gt_data["r1"]
r_34 = fp.rs = gt_data["rs"]
offset = fp.offset = gt_data["offset"]
arcs = []
if offset == 0.0:
phi5 = np.pi / fp.teeth
ref = reflection(-phi5 - np.pi / 2.0)
rp = pitch * fp.teeth / np.pi / 2.0 - u
m_34 = np.array([-(r_12 + r_34), rp - h + r_12])
x2 = np.array([-r_12, m_34[1]])
x4 = np.array([m_34[0], m_34[1] + r_34])
x6 = ref(x4)
mir = np.array([-1.0, 1.0])
xn2 = mir * x2
xn4 = mir * x4
mn_34 = mir * m_34
arcs.append(part_arc_from_points_and_center(xn4, xn2, mn_34).toShape())
arcs.append(
Part.Arc(
App.Vector(*xn2, 0.0),
App.Vector(0, rp - h, 0.0),
App.Vector(*x2, 0.0),
).toShape()
)
arcs.append(part_arc_from_points_and_center(x2, x4, m_34).toShape())
arcs.append(
part_arc_from_points_and_center(x4, x6, np.array([0.0, 0.0])).toShape()
)
else:
phi_12 = np.arctan(np.sqrt(1.0 / (((r_12 - r_23) / offset) ** 2 - 1)))
rp = pitch * fp.teeth / np.pi / 2.0
r4 = r5 = rp - u
m_12 = np.array([0.0, r5 - h + r_12])
m_23 = np.array([offset, offset / np.tan(phi_12) + m_12[1]])
m_23y = m_23[1]
# solving for phi4:
# sympy.solve(
# ((r5 - r_34) * sin(phi4) + offset) ** 2 + \
# ((r5 - r_34) * cos(phi4) - m_23y) ** 2 - \
# ((r_34 + r_23) ** 2), phi4)
phi4 = 2 * np.arctan(
(
-2 * offset * r5
+ 2 * offset * r_34
+ np.sqrt(
-(m_23y**4)
- 2 * m_23y**2 * offset**2
+ 2 * m_23y**2 * r5**2
- 4 * m_23y**2 * r5 * r_34
+ 2 * m_23y**2 * r_23**2
+ 4 * m_23y**2 * r_23 * r_34
+ 4 * m_23y**2 * r_34**2
- offset**4
+ 2 * offset**2 * r5**2
- 4 * offset**2 * r5 * r_34
+ 2 * offset**2 * r_23**2
+ 4 * offset**2 * r_23 * r_34
+ 4 * offset**2 * r_34**2
- r5**4
+ 4 * r5**3 * r_34
+ 2 * r5**2 * r_23**2
+ 4 * r5**2 * r_23 * r_34
- 4 * r5**2 * r_34**2
- 4 * r5 * r_23**2 * r_34
- 8 * r5 * r_23 * r_34**2
- r_23**4
- 4 * r_23**3 * r_34
- 4 * r_23**2 * r_34**2
)
)
/ (
m_23y**2
+ 2 * m_23y * r5
- 2 * m_23y * r_34
+ offset**2
+ r5**2
- 2 * r5 * r_34
- r_23**2
- 2 * r_23 * r_34
)
)
phi5 = np.pi / fp.teeth
m_34 = (r5 - r_34) * np.array([-np.sin(phi4), np.cos(phi4)])
x2 = np.array([-r_12 * np.sin(phi_12), m_12[1] - r_12 * np.cos(phi_12)])
x3 = m_34 + r_34 / (r_34 + r_23) * (m_23 - m_34)
x4 = r4 * np.array([-np.sin(phi4), np.cos(phi4)])
ref = reflection(-phi5 - np.pi / 2)
x6 = ref(x4)
mir = np.array([-1.0, 1.0])
xn2 = mir * x2
xn3 = mir * x3
xn4 = mir * x4
mn_34 = mir * m_34
mn_23 = mir * m_23
arcs.append(part_arc_from_points_and_center(xn4, xn3, mn_34).toShape())
arcs.append(part_arc_from_points_and_center(xn3, xn2, mn_23).toShape())
arcs.append(part_arc_from_points_and_center(xn2, x2, m_12).toShape())
arcs.append(part_arc_from_points_and_center(x2, x3, m_23).toShape())
arcs.append(part_arc_from_points_and_center(x3, x4, m_34).toShape())
arcs.append(
part_arc_from_points_and_center(x4, x6, np.array([0.0, 0.0])).toShape()
)
wire = Part.Wire(arcs)
wires = [wire]
rot = App.Matrix()
rot.rotateZ(np.pi * 2 / fp.teeth)
for _ in range(fp.teeth - 1):
wire = wire.transformGeometry(rot)
wires.append(wire)
wi = Part.Wire(wires)
if fp.height.Value == 0:
return wi
else:
return Part.Face(wi).extrude(App.Vector(0, 0, fp.height))

View File

@@ -23,12 +23,9 @@ import scipy as sp
import FreeCAD as App
import Part
from pygears._functions import (
rotation,
reflection
)
from pygears._functions import rotation, reflection
from .features import BaseGear, fcvec
from .basegear import BaseGear, fcvec
class TimingGearT(BaseGear):
@@ -36,8 +33,15 @@ class TimingGearT(BaseGear):
print("hello gear")
obj.addProperty("App::PropertyLength", "pitch", "base", "pitch of gear")
obj.addProperty("App::PropertyInteger", "teeth", "base", "number of teeth")
obj.addProperty("App::PropertyLength", "tooth_height", "base", "radial height of tooth")
obj.addProperty("App::PropertyLength", "u", "base", "radial distance from tooth-head to pitch circle")
obj.addProperty(
"App::PropertyLength", "tooth_height", "base", "radial height of tooth"
)
obj.addProperty(
"App::PropertyLength",
"u",
"base",
"radial distance from tooth-head to pitch circle",
)
obj.addProperty("App::PropertyAngle", "alpha", "base", "angle of tooth flanks")
obj.addProperty("App::PropertyLength", "height", "base", "extrusion height")
obj.pitch = "5. mm"
@@ -55,23 +59,23 @@ class TimingGearT(BaseGear):
teeth = fp.teeth
u = fp.u.Value
tooth_height = fp.tooth_height.Value
alpha = fp.alpha.Value / 180. * np.pi # we need radiant
alpha = fp.alpha.Value / 180.0 * np.pi # we need radiant
height = fp.height.Value
r_p = pitch * teeth / 2. / np.pi
r_p = pitch * teeth / 2.0 / np.pi
gamma_0 = pitch / r_p
gamma_1 = gamma_0 / 4
p_A = np.array([
np.cos(-gamma_1),
np.sin(-gamma_1)
]) * (r_p - u - tooth_height / 2)
p_A = np.array([np.cos(-gamma_1), np.sin(-gamma_1)]) * (
r_p - u - tooth_height / 2
)
def line(s):
p = p_A + np.array([
np.cos(alpha / 2 - gamma_1),
np.sin(alpha / 2 - gamma_1)
]) * s
p = (
p_A
+ np.array([np.cos(alpha / 2 - gamma_1), np.sin(alpha / 2 - gamma_1)])
* s
)
return p
def dist_p1(s):
@@ -80,13 +84,13 @@ class TimingGearT(BaseGear):
def dist_p2(s):
return (np.linalg.norm(line(s)) - (r_p - u)) ** 2
s1 = sp.optimize.minimize(dist_p1, 0.).x
s2 = sp.optimize.minimize(dist_p2, 0.).x
s1 = sp.optimize.minimize(dist_p1, 0.0).x
s2 = sp.optimize.minimize(dist_p2, 0.0).x
p_1 = line(s1)
p_2 = line(s2)
mirror = reflection(0.) # reflect the points at the x-axis
mirror = reflection(0.0) # reflect the points at the x-axis
p_3, p_4 = mirror(np.array([p_2, p_1]))
rot = rotation(-gamma_0) # why is the rotation in wrong direction ???
@@ -110,4 +114,4 @@ class TimingGearT(BaseGear):
return contour
else:
face = Part.Face(Part.Wire(wires))
return face.extrude(App.Vector(0., 0., height))
return face.extrude(App.Vector(0.0, 0.0, height))

151
freecad/gears/wormgear.py Normal file
View File

@@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * This program is free software: you can redistribute it and/or modify *
# * it under the terms of the GNU General Public License as published by *
# * the Free Software Foundation, either version 3 of the License, or *
# * (at your option) any later version. *
# * *
# * This program is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU General Public License for more details. *
# * *
# * You should have received a copy of the GNU General Public License *
# * along with this program. If not, see <http://www.gnu.org/licenses/>. *
# * *
# ***************************************************************************
import FreeCAD as App
import Part
import numpy as np
from pygears.involute_tooth import InvoluteTooth
from pygears._functions import rotation
from .basegear import BaseGear, helicalextrusion, fcvec
class WormGear(BaseGear):
"""FreeCAD gear rack"""
def __init__(self, obj):
super(WormGear, self).__init__(obj)
obj.addProperty("App::PropertyInteger", "teeth", "base", "number of teeth")
obj.addProperty("App::PropertyLength", "module", "base", "module")
obj.addProperty("App::PropertyLength", "height", "base", "height")
obj.addProperty("App::PropertyLength", "diameter", "base", "diameter")
obj.addProperty("App::PropertyAngle", "beta", "computed", "beta ", 1)
obj.addProperty(
"App::PropertyAngle", "pressure_angle", "involute", "pressure angle"
)
obj.addProperty(
"App::PropertyBool", "reverse_pitch", "base", "reverse rotation of helix"
)
obj.addProperty(
"App::PropertyFloat",
"head",
"tolerance",
"head * module = additional length of head",
)
obj.addProperty(
"App::PropertyFloat",
"clearance",
"tolerance",
"clearance * module = additional length of root",
)
obj.teeth = 3
obj.module = "1. mm"
obj.pressure_angle = "20. deg"
obj.height = "5. mm"
obj.diameter = "5. mm"
obj.clearance = 0.25
obj.head = 0
obj.reverse_pitch = False
self.obj = obj
obj.Proxy = self
def generate_gear_shape(self, fp):
m = fp.module.Value
d = fp.diameter.Value
t = fp.teeth
h = fp.height
clearance = fp.clearance
head = fp.head
alpha = fp.pressure_angle.Value
beta = np.arctan(m * t / d)
fp.beta = np.rad2deg(beta)
beta = -(fp.reverse_pitch * 2 - 1) * (np.pi / 2 - beta)
r_1 = (d - (2 + 2 * clearance) * m) / 2
r_2 = (d + (2 + 2 * head) * m) / 2
z_a = (2 + head + clearance) * m * np.tan(np.deg2rad(alpha))
z_b = (m * np.pi - 4 * m * np.tan(np.deg2rad(alpha))) / 2
z_0 = clearance * m * np.tan(np.deg2rad(alpha))
z_1 = z_b - z_0
z_2 = z_1 + z_a
z_3 = z_2 + z_b - 2 * head * m * np.tan(np.deg2rad(alpha))
z_4 = z_3 + z_a
def helical_projection(r, z):
phi = 2 * z / m / t
x = r * np.cos(phi)
y = r * np.sin(phi)
z = 0 * y
return np.array([x, y, z]).T
# create a circle from phi=0 to phi_1 with r_1
phi_0 = 2 * z_0 / m / t
phi_1 = 2 * z_1 / m / t
c1 = Part.makeCircle(
r_1,
App.Vector(0, 0, 0),
App.Vector(0, 0, 1),
np.rad2deg(phi_0),
np.rad2deg(phi_1),
)
# create first bspline
z_values = np.linspace(z_1, z_2, 10)
r_values = np.linspace(r_1, r_2, 10)
points = helical_projection(r_values, z_values)
bsp1 = Part.BSplineCurve()
bsp1.interpolate(list(map(fcvec, points)))
bsp1 = bsp1.toShape()
# create circle from phi_2 to phi_3
phi_2 = 2 * z_2 / m / t
phi_3 = 2 * z_3 / m / t
c2 = Part.makeCircle(
r_2,
App.Vector(0, 0, 0),
App.Vector(0, 0, 1),
np.rad2deg(phi_2),
np.rad2deg(phi_3),
)
# create second bspline
z_values = np.linspace(z_3, z_4, 10)
r_values = np.linspace(r_2, r_1, 10)
points = helical_projection(r_values, z_values)
bsp2 = Part.BSplineCurve()
bsp2.interpolate(list(map(fcvec, points)))
bsp2 = bsp2.toShape()
wire = Part.Wire([c1, bsp1, c2, bsp2])
w_all = [wire]
rot = App.Matrix()
rot.rotateZ(2 * np.pi / t)
for i in range(1, t):
w_all.append(w_all[-1].transformGeometry(rot))
full_wire = Part.Wire(w_all)
if h == 0:
return full_wire
else:
shape = helicalextrusion(Part.Face(full_wire), h, h * np.tan(beta) * 2 / d)
return shape

View File

@@ -9,6 +9,6 @@ setup(
maintainer_email="sppedflyer@gmail.com",
url="https://github.com/looooo/FCGear",
description="gears for FreeCAD",
install_requires=["numpy"],
install_requires=["numpy", "scipy"],
include_package_data=True,
)