diff --git a/freecad/gears/bevelgear.py b/freecad/gears/bevelgear.py
new file mode 100644
index 0000000..86b6abd
--- /dev/null
+++ b/freecad/gears/bevelgear.py
@@ -0,0 +1,207 @@
+
+# -*- 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 . *
+# * *
+# ***************************************************************************
+
+import FreeCAD as App
+import Part
+
+import numpy as np
+from pygears.bevel_tooth import BevelTooth
+from pygears._functions import rotation3D
+
+from .features 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)
+
diff --git a/freecad/gears/commands.py b/freecad/gears/commands.py
index 592b798..197b208 100644
--- a/freecad/gears/commands.py
+++ b/freecad/gears/commands.py
@@ -22,20 +22,21 @@ import FreeCADGui as Gui
from .features 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
diff --git a/freecad/gears/connector.py b/freecad/gears/connector.py
index 394cb03..578acb7 100644
--- a/freecad/gears/connector.py
+++ b/freecad/gears/connector.py
@@ -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
@@ -42,12 +42,20 @@ 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 __getstate__(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}
+
+ def __setstate__(self, state):
+ self.icon_fn = state["icon_fn"]
- def __setstate__(self, state):
- self.icon_fn = state["icon_fn"]
class GearConnector(object):
diff --git a/freecad/gears/crowngear.py b/freecad/gears/crowngear.py
new file mode 100644
index 0000000..cee4d8d
--- /dev/null
+++ b/freecad/gears/crowngear.py
@@ -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 . *
+# * *
+# ***************************************************************************
+
+import os
+import sys
+
+import FreeCAD as App
+import Part
+
+import numpy as np
+
+from .features 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)
diff --git a/freecad/gears/cycloidgear.py b/freecad/gears/cycloidgear.py
new file mode 100644
index 0000000..89c58a5
--- /dev/null
+++ b/freecad/gears/cycloidgear.py
@@ -0,0 +1,194 @@
+
+# -*- 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 . *
+# * *
+# ***************************************************************************
+
+import FreeCAD as App
+import Part
+
+import numpy as np
+from pygears.cycloid_tooth import CycloidTooth
+from pygears._functions import rotation
+
+from .features 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)
+
diff --git a/freecad/gears/cycloidgearrack.py b/freecad/gears/cycloidgearrack.py
new file mode 100644
index 0000000..e5f2dff
--- /dev/null
+++ b/freecad/gears/cycloidgearrack.py
@@ -0,0 +1,237 @@
+# -*- 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 . *
+# * *
+# ***************************************************************************
+
+import os
+import sys
+
+import FreeCAD as App
+import Part
+
+import numpy as np
+
+from pygears._functions import reflection
+from .features 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
diff --git a/freecad/gears/features.py b/freecad/gears/features.py
index 19209f5..61d3a22 100644
--- a/freecad/gears/features.py
+++ b/freecad/gears/features.py
@@ -19,6 +19,9 @@
import os
import sys
+import FreeCAD as App
+import Part
+
import numpy as np
import math
from pygears import __version__
@@ -33,35 +36,6 @@ from pygears._functions import (
)
-import FreeCAD as App
-import Part
-from Part import (
- BSplineCurve,
- Shape,
- Wire,
- Face,
- makePolygon,
- makeLoft,
- BSplineSurface,
- makePolygon,
- makeHelix,
- makeShell,
- makeSolid,
- LineSegment,
-)
-
-
-__all__ = [
- "InvoluteGear",
- "CycloidGear",
- "BevelGear",
- "InvoluteGearRack",
- "CrownGear",
- "WormGear",
- "HypoCycloidGear",
- "ViewProviderGear",
-]
-
def fcvec(x):
if len(x) == 2:
@@ -170,1719 +144,6 @@ class BaseGear(object):
pass
-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 = Wire(edges)
- profile = rotate_tooth(tooth, obj.teeth)
-
- if obj.height.Value == 0:
- return profile
- base = 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)
-
-
-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 = Wire(edges)
- profile = rotate_tooth(tooth, fp.teeth)
- if fp.height.Value == 0:
- return Part.makeCompound([outer_circle, profile])
- base = 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 = Face([outer_circle, inner_circle])
- return base.extrude(App.Vector(0, 0, fp.height.Value))
-
- def __getstate__(self):
- return None
-
- def __setstate__(self, state):
- return None
-
-
-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 = 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 = 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] = Wire(teeth[-1].Edges[:-1])
-
- if obj.add_endings:
- teeth = [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 + [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 = Wire([bottom] + teeth)
-
- if obj.height.Value == 0:
- return pol
- elif obj.beta.Value == 0:
- face = Face(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 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 makeLoft([pol, pol2], True)
-
-
-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 = 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] = Wire(teeth[-1].Edges[:-1])
-
- if obj.add_endings:
- teeth = [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 + [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 = Wire([bottom] + teeth)
-
- if obj.height.Value == 0:
- return pol
- elif obj.beta.Value == 0:
- face = Face(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 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 makeLoft([pol, pol2], True)
-
- def __getstate__(self):
- return None
-
- def __setstate__(self, state):
- return None
-
-
-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 = Wire(makePolygon(list(map(fcvec, pts))))
- polies.append(poly)
- loft = 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)
-
-
-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 = Wire(edges)
-
- profile = rotate_tooth(tooth, fp.teeth)
- if fp.height.Value == 0:
- return profile
- base = 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)
-
-
-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 = 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 = BSplineSurface()
- b.interpolate(i)
- surfs.append(b)
- return Shape(surfs)
-
- def spherical_rot(self, point, phi):
- new_phi = np.sqrt(np.linalg.norm(point)) * phi
- return rotation3D(new_phi)(point)
-
-
-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(Part.Wire(w_all))
- if h == 0:
- return full_wire
- else:
- shape = helicalextrusion(Face(full_wire), h, h * np.tan(beta) * 2 / d)
- return shape
-
-
-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))
-
-
class LanternGear(BaseGear):
def __init__(self, obj):
super(LanternGear, self).__init__(obj)
@@ -1958,14 +219,14 @@ class LanternGear(BaseGear):
xy1 = np.array([x, y]).T
p_1 = xy1[0]
p_1_end = xy1[-1]
- bsp_1 = BSplineCurve()
+ 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 = BSplineCurve()
+ bsp_2 = Part.BSplineCurve()
bsp_2.interpolate(list(map(fcvec, xy2)))
w_2 = bsp_2.toShape()
@@ -2161,8 +422,8 @@ class HypoCycloidGear(BaseGear):
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
- # Wire(Part.makeCircle(minRadius,App.Vector(-e, 0, 0)))
- # Wire(Part.makeCircle(maxRadius,App.Vector(-e, 0, 0)))
+ # 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
@@ -2187,11 +448,11 @@ class HypoCycloidGear(BaseGear):
wi = wi.transformGeometry(mat)
wires.append(wi)
- cam = Face(Wire(wires))
+ cam = Part.Face(Part.Wire(wires))
# add a circle in the center of the cam
if fp.hole_radius.Value:
- centerCircle = Face(
- Wire(Part.makeCircle(fp.hole_radius.Value, App.Vector(-e, 0, 0)))
+ centerCircle = Part.Face(
+ Part.Wire(Part.makeCircle(fp.hole_radius.Value, App.Vector(-e, 0, 0)))
)
cam = cam.cut(centerCircle)
@@ -2227,9 +488,9 @@ class HypoCycloidGear(BaseGear):
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(Wire(Part.makeCircle(d / 2, App.Vector(x, y, 0))))
+ pins.append(Part.Wire(Part.makeCircle(d / 2, App.Vector(x, y, 0))))
- pins = Face(pins)
+ pins = Part.Face(pins)
z_offset = -fp.pin_height.Value / 2
if fp.center_pins == True:
@@ -2319,21 +580,21 @@ def helicalextrusion(face, height, angle, double_helix=False):
def make_face(edge1, edge2):
v1, v2 = edge1.Vertexes
v3, v4 = edge2.Vertexes
- e1 = Wire(edge1)
- e2 = LineSegment(v1.Point, v3.Point).toShape().Edges[0]
+ e1 = Part.Wire(edge1)
+ e2 = Part.LineSegment(v1.Point, v3.Point).toShape().Edges[0]
e3 = edge2
- e4 = LineSegment(v4.Point, v2.Point).toShape().Edges[0]
- w = Wire([e3, e4, e1, e2])
- return Face(w)
+ 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 = BSplineCurve()
+ out = Part.BSplineCurve()
out.interpolate(list(map(fcvec, i)))
wi.append(out.toShape())
- return Wire(wi)
+ return Part.Wire(wi)
def points_to_wire(pts):
@@ -2341,12 +602,12 @@ def points_to_wire(pts):
for i in pts:
if len(i) == 2:
# straight edge
- out = LineSegment(*list(map(fcvec, i)))
+ out = Part.LineSegment(*list(map(fcvec, i)))
else:
- out = BSplineCurve()
+ out = Part.BSplineCurve()
out.interpolate(list(map(fcvec, i)))
wire.append(out.toShape())
- return Wire(wire)
+ return Part.Wire(wire)
def rotate_tooth(base_tooth, num_teeth):
@@ -2355,7 +616,7 @@ def rotate_tooth(base_tooth, num_teeth):
flat_shape = [base_tooth]
for t in range(num_teeth - 1):
flat_shape.append(flat_shape[-1].transformGeometry(rot))
- return Wire(flat_shape)
+ return Part.Wire(flat_shape)
def fillet_between_edges(edge_1, edge_2, radius):
diff --git a/freecad/gears/hypocycloidgear.py b/freecad/gears/hypocycloidgear.py
new file mode 100644
index 0000000..c0c5e1a
--- /dev/null
+++ b/freecad/gears/hypocycloidgear.py
@@ -0,0 +1,284 @@
+
+# -*- 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 . *
+# * *
+# ***************************************************************************
+
+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 .features 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)
+
diff --git a/freecad/gears/internalinvolutegear.py b/freecad/gears/internalinvolutegear.py
new file mode 100644
index 0000000..78d169f
--- /dev/null
+++ b/freecad/gears/internalinvolutegear.py
@@ -0,0 +1,249 @@
+
+# -*- 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 . *
+# * *
+# ***************************************************************************
+
+import FreeCAD as App
+import Part
+
+import numpy as np
+from pygears.involute_tooth import InvoluteTooth
+from pygears._functions import rotation
+
+from .features 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))
+
diff --git a/freecad/gears/involutegear.py b/freecad/gears/involutegear.py
new file mode 100644
index 0000000..480ae08
--- /dev/null
+++ b/freecad/gears/involutegear.py
@@ -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 . *
+# * *
+# ***************************************************************************
+
+import FreeCAD as App
+import Part
+
+import numpy as np
+from pygears.involute_tooth import InvoluteTooth
+from pygears._functions import rotation
+
+from .features 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)
diff --git a/freecad/gears/involutegearrack.py b/freecad/gears/involutegearrack.py
new file mode 100644
index 0000000..24325b9
--- /dev/null
+++ b/freecad/gears/involutegearrack.py
@@ -0,0 +1,245 @@
+
+# -*- 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 . *
+# * *
+# ***************************************************************************
+
+
+import FreeCAD as App
+import Part
+
+import numpy as np
+from pygears.involute_tooth import InvoluteRack
+
+from .features 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)
+
diff --git a/freecad/gears/lanterngear.py b/freecad/gears/lanterngear.py
new file mode 100644
index 0000000..ad2fcd0
--- /dev/null
+++ b/freecad/gears/lanterngear.py
@@ -0,0 +1,134 @@
+
+# -*- 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 . *
+# * *
+# ***************************************************************************
+
+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 .features 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))
+
diff --git a/freecad/gears/timinggear.py b/freecad/gears/timinggear.py
new file mode 100644
index 0000000..c30cbc2
--- /dev/null
+++ b/freecad/gears/timinggear.py
@@ -0,0 +1,290 @@
+
+# -*- 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 . *
+# * *
+# ***************************************************************************
+
+import FreeCAD as App
+import Part
+
+import numpy as np
+
+from pygears._functions import reflection
+from .features 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))
+
diff --git a/freecad/gears/timing_gear_t.py b/freecad/gears/timinggear_t.py
similarity index 100%
rename from freecad/gears/timing_gear_t.py
rename to freecad/gears/timinggear_t.py
diff --git a/freecad/gears/wormgear.py b/freecad/gears/wormgear.py
new file mode 100644
index 0000000..3a9600e
--- /dev/null
+++ b/freecad/gears/wormgear.py
@@ -0,0 +1,152 @@
+
+# -*- 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 . *
+# * *
+# ***************************************************************************
+
+import FreeCAD as App
+import Part
+
+import numpy as np
+from pygears.involute_tooth import InvoluteTooth
+from pygears._functions import rotation
+
+from .features 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
diff --git a/setup.py b/setup.py
index ab51b7f..e91e7dc 100644
--- a/setup.py
+++ b/setup.py
@@ -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,
)