From 055aab2e078a55e3737ab9d0c0ec21589f0992f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=A4hr?= Date: Mon, 13 Sep 2021 00:38:27 +0200 Subject: [PATCH] Build the helical internal gear directly, not via boolen cut The original, now replaced, way had the benefit of reusing the existing helicalextrusion method, but the following boolean cut operation was a) slow and b) suffered from co-planar issues. The new way extrudes the face directly which does not have these shortcommings. Unfortunaly, we had to use a different API: the original one is simple and straight forward, gives no easy way to access the end of the sweep. Collecting the wires manually, filtering on the position of all vertexes, unvailed tolerances heigher then 0.1, so that a face created from it was not planar and prevented ceating a valid solid. The now used API requires more manual work in the initial setup, but the end wires for construction the top face are directly accesible and the tolerances are below 0.001 so we can create a planar face and valid solid. This new way also much faster (1.6 sec vs. 5.0 sec on my machine using default gear parameters on a double helix, naively measured via `time.perf_counter()`). Currently, the new helical extusion method is only used for the internal gear. Migrating the other usages will be done as a separate commit. --- freecad/gears/features.py | 62 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/freecad/gears/features.py b/freecad/gears/features.py index 3ee73ae..6d8ed9d 100644 --- a/freecad/gears/features.py +++ b/freecad/gears/features.py @@ -398,9 +398,9 @@ class InternalInvoluteGear(BaseGear): sh = Face([outer_circle, wi]) return sh.extrude(App.Vector(0, 0, fp.height.Value)) else: - tool = helicalextrusion( - wi, fp.height.Value, fp.height.Value * np.tan(fp.gear.beta) * 2 / fp.gear.d, fp.double_helix) - return Part.makeCylinder(fp.outside_diameter / 2., fp.height.Value).cut(tool) + sh = Face([outer_circle, wi]) + twist_angle = fp.height.Value * np.tan(fp.gear.beta) * 2 / fp.gear.d + return helicalextrusion2(sh, fp.height.Value, twist_angle, fp.double_helix) else: inner_circle = Part.Wire(Part.makeCircle(fp.dw / 2.)) inner_circle.reverse() @@ -1437,6 +1437,62 @@ def helicalextrusion(wire, height, angle, double_helix=False): first_solid = first_spine.makePipeShell([wire], True, True) return first_solid +def helicalextrusion2(face, height, angle, double_helix=False): + """ + A helical extrusion using the BRepOffsetAPI + face -- the face to extrude (may contain holes, i.e. more then one wires) + height -- the hight of the extrusion, normal to the face + angle -- the twist angle of the extrusion in radians + + returns a solid + """ + pitch = height * 2 * np.pi / abs(angle) + radius = 10.0 # as we are only interested in the "twist", we take an arbitrary constant here + cone_angle = 0 + direction = bool(angle < 0) + if double_helix: + spine = Part.makeHelix(pitch, height / 2.0, radius, cone_angle, direction) + else: + spine = Part.makeHelix(pitch, height, radius, cone_angle, direction) + def make_pipe(path, profile): + """ + returns (shell, last_wire) + """ + mkPS = Part.BRepOffsetAPI.MakePipeShell(path) + mkPS.setFrenetMode(True) # otherwise, the profile's normal would follow the path + mkPS.add(profile, False, False) + mkPS.build() + return (mkPS.shape(), mkPS.lastShape()) + shell_faces = [] + top_wires = [] + for wire in face.Wires: + pipe_shell, top_wire = make_pipe(spine, wire) + shell_faces.extend(pipe_shell.Faces) + top_wires.append(top_wire) + top_face = Part.Face(top_wires) + shell_faces.append(top_face) + if double_helix: + origin = App.Vector(0, 0, 0) + xy_normal = App.Vector(0, 0, 1) + mirror_xy = lambda f: f.mirror(origin, xy_normal) + bottom_faces = list(map(mirror_xy, shell_faces)) + shell_faces.extend(bottom_faces) + matrix = App.Matrix() + matrix.move(App.Vector(0, 0, height / 2.0)) + move_up = lambda f: f.transformGeometry(matrix) + shell_faces = list(map(move_up, shell_faces)) + else: + shell_faces.append(face) # the bottom is what we extruded + shell = Part.makeShell(shell_faces) + # TODO: why the heck is this shell empty if double_helix??? + if len(shell.Faces) == 0: + # ... and why the heck does it work when making an intermediate compound??? + hacky_intermediate_compound = Part.makeCompound(shell_faces) + shell = Part.makeShell(hacky_intermediate_compound.Faces) + App.Console.PrintMessage(f"shell.Faces from compound: {len(shell.Faces)}\n") + #shell.sewShape() # fill gaps that may result from accumulated tolerances. Needed? + #shell = shell.removeSplitter() # refine. Needed? + return Part.makeSolid(shell) def make_face(edge1, edge2): v1, v2 = edge1.Vertexes