# -*- coding: utf-8 -*- # Author: Ruslan Krenzler. # Date: 2 December 2017 # Create a pipe elbow. # Version 0.2 import math import FreeCAD import Spreadsheet import Sketcher import Part # Here are dimensions used to create a elbow. alpha = "60 deg" r1 = "1 in" r2 = "2 in" r3 = "3 in" len1 = "4 in" len2 = "5 in" tu = FreeCAD.Units.parseQuantity # Add sketches for swept part (circle) into X-Y Plane. document = App.activeDocument() class Elbow: # This factor is used to slightly change the size of a subtracted part # to prevent problems with boolean operations. # This value does not change the appearance of part and can be large. RELATIVE_EPSILON = 1 # The outer radius of the bent part must be slightly smaller # than the outer radius of the pipe parts. Otherwise # there can be problems with fusion of the elbow parts. # This value *changes* the appearance of the part. # If this value is 0.00001 the bent part does not have a whole for some reasons. BENT_RELATIVE_EPSILON = 0.0001 def __init__ (self, document): self.document = document # Set default values. self.alpha = "60 deg" self.r1 = "1 in" self.r2 = "2 in" self.r3 = "3 in" self.len1 = "4 in" self.len2 = "5 in" @staticmethod def createBentCylinder(document, group, r_cyl, r_bent, alpha, forSubstraction = False): """Create a cylinder with a radius r_cyl1 with a base on X-Y plane and bent it by angle alpha along radius r_bent. Add all new created objects to a group. This should simplify the cleaning up of the intermediate parts. :param r_cyl: radius of the cylinder in Base.Quantity :param r_bent: radius of the path in Base.Quantity :param alpha: in Base.Quantity :param forSubstraction When creating a bent part, which will be subtracted, it is possible to add an additional 2epsilon of length to the both sides of the arc. In this case The angle of the created bent cylinder is alpha + 2 epsilon. """ base_sketch = document.addObject('Sketcher::SketchObject','BaseSketch') base_sketch.Placement = App.Placement(App.Vector(0.000000,0.000000,0.000000),App.Rotation(0.000000,0.000000,0.000000,1.000000)) # When adding a radius, do not forget to convert the units. base_sketch.addGeometry(Part.Circle(App.Vector(0.000000,0.000000,0),App.Vector(0,0,1),r_cyl),False) # Add sweeping part into X-Z plane. path_sketch = document.addObject('Sketcher::SketchObject','PathSketch') path_sketch.Placement = App.Placement(App.Vector(0.000000,0.000000,0.000000),App.Rotation(-0.707107,0.000000,0.000000,-0.707107)) # The arc is created counter clockwise. rel_epsilon = 0.0 if forSubstraction: rel_epsilon = 0.1 # Do not use # The start and stop angle must be converted to values in rad. # For some reasons Part.ArcOfCircle(Part.Circle(App.Vector(r_bent,0,0),App.Vector(0,0,1),r_bent),0, tu("pi rad").getValueAs("rad")),False) # And Part.ArcOfCircle(Part.Circle(App.Vector(r_bent,0,0),App.Vector(0,0,1),r_bent),0, tu("pi rad")),False) # produces different results. start = (tu("pi rad") - alpha).getValueAs("rad")*(1-rel_epsilon) stop = tu("pi rad").getValueAs("rad")*(1+rel_epsilon) path_sketch.addGeometry(Part.ArcOfCircle(Part.Circle(App.Vector(r_bent,0,0),App.Vector(0,0,1),r_bent),start, stop),False) # Sweep the parts. sweep = document.addObject('Part::Sweep','Sweep') sweep.Sections=[base_sketch, ] sweep.Spine=(path_sketch,["Edge1"]) sweep.Solid=True sweep.Frenet=False # Add all the objects to the group. group.addObject(base_sketch) group.addObject(path_sketch,) group.addObject(sweep) return sweep @staticmethod def createBentPart(document, group, r1, r3, alpha): # Make the outer part a little bit smaller, in order to prevent # problems with union of the pipe parts. corrected_outer_radius = tu(r3)*(1-Elbow.BENT_RELATIVE_EPSILON) outer_sweep = Elbow.createBentCylinder(document, group, corrected_outer_radius, tu(r3), tu(alpha), False) inner_sweep = Elbow.createBentCylinder(document, group, tu(r1), tu(r3), tu(alpha), True) bent_cut = document.addObject("Part::Cut","BentCut") bent_cut.Base = outer_sweep bent_cut.Tool = inner_sweep group.addObject(bent_cut) return bent_cut # What is the proper name of the part where the pipe comes in? # Provisionally, I will call it "pipe". @staticmethod def createPipePart(document, outer_r, inner_r, length): """Create a pipe, which is a difference between two cylinders It is better when the cylinder which is subtracted will not share the same surface as the other cylinder to prevent problems. Here we extend the subtracted cylinder by epsilon from both ends. """ outer_cylinder = document.addObject("Part::Cylinder","OuterCylinder") outer_cylinder.Radius = outer_r outer_cylinder.Height = length # Make the inner cylider a little bit longer in both directions. epsilon = tu(length)*Elbow.RELATIVE_EPSILON inner_cylinder = document.addObject("Part::Cylinder","InnerCylinder") inner_cylinder.Radius = inner_r inner_cylinder.Height = tu(length) + 2*epsilon inner_cylinder.Placement.Base = App.Vector(0,0, -epsilon) cut = document.addObject("Part::Cut","PipeCut") cut.Base = outer_cylinder cut.Tool = inner_cylinder return cut @staticmethod def toSolid(document, part, name): """Convert object to a solid. Basically those are commands, which FreeCAD runs when you convert a part to a solid. Important!: recompute document. Without this step. The faces may not yet exists. (Is my interpretation correct?) document.recompute() """ s = part.Shape.Faces s = Part.Solid(Part.Shell(s)) o = document.addObject("Part::Feature", name) o.Label=name o.Shape=s return o @staticmethod def NestedObjects(group): res = [] if group.OutList == []: res.append(group) else: # Append children first. for o in group.OutList: res += Elbow.NestedObjects(o) res.append(group) return res def create(self, convertToSolid = True): """Create elbow.""" # Create new group to put all the temporal data. group = self.document.addObject("App::DocumentObjectGroup", "elbow group") # Create the bent part. bent_part = self.createBentPart(self.document, group, self.r1, self.r3, self.alpha) pipe1 = self.createPipePart(self.document, self.r3, self.r2, self.len1) # Move pipe1 to the bottom such that its upper part lies in X-Y plane. pipe1.Placement.Base = (0,0,-tu(self.len1)) # Rotate and move the second pipe, such that it touches with its base the bent part. pipe2 = self.createPipePart(self.document, self.r3, self.r2, self.len2) x = (1-math.cos(tu(self.alpha).getValueAs("rad"))) *tu(self.r3) z = math.sin(tu(self.alpha).getValueAs("rad"))*tu(self.r3) pipe2.Placement = App.Placement(App.Vector(x,0,z),App.Rotation(App.Vector(0,1,0),tu(self.alpha))) # Add pipes to a group. group.addObject(pipe1) group.addObject(pipe2) if convertToSolid: # Remove objects of the pipes separately. # Create all faces. document.recompute() # Should I really recompute all the document? # Now convert all parts to solid, and remove intermediate data. pipe1_solid = self.toSolid(document, pipe1, "pipe1 (solid)") bent_solid = self.toSolid(document, bent_part, "bent part (solid)") pipe2_solid = self.toSolid(document, pipe2, "pipe2 (solid)") # Fuse the elements. fusion = App.activeDocument().addObject("Part::MultiFuse","FusionElbow") fusion.Shapes = [pipe1_solid, bent_solid, pipe2_solid,] group.addObject(fusion) document.recompute() fusion_solid = self.toSolid(document, fusion, "elbow (solid)") # Remove previous (intermediate parts). parts = Elbow.NestedObjects(group) # Document.removeObjects can remove multple objects, when we use # parts directly. To prevent exceptions with deleted objects, # use the name list instead. names_to_remove = [] for part in parts: if part.Name not in names_to_remove: names_to_remove.append(part.Name) for name in names_to_remove: print("Deleting temporary objects %s."%name) document.removeObject(name) return fusion_solid return group # Create multiple parts for testing. It is computation intensive. def Test(): ell = Elbow(document) x = tu("0 in") x_step = tu("20 in") # Create multiple parts for test purpose for alpha_deg in [30,45,60,90,110]: alpha = tu("%d deg"%alpha_deg) y = tu("0 in") for r3_in in range(1,10): r3 = tu("%d in"%r3_in) y = y + r3 # Move to the next position (part I). r2 = r3*2/3 r1 = r3/3 ell.alpha = str(alpha) ell.r3 = str(r3) ell.r2 = str(r2) ell.r1 = str(r1) label = "Angle: %s, Outer radius: %s"%(alpha.getUserPreferred()[0], r3.getUserPreferred()[0]) print("Creating... "+ label) part = ell.create(True) # Create a solid. #Add corresponding label to the part. part.Label = label Draft.move(part,FreeCAD.Vector(x,y,0),copy=False) y = y + r3 # Move to the next position (part II). x = x + x_step # Dimensions of 1" SCH40 PVC pipe. #['1"', 40, 1.315, 1.029, 0.133, 0.333, 450], ell = Elbow(document) ell.alpha = "45 deg" ell.r3 = "2 in" ell.r2 = "1.315 in" ell.r1 = "1.029 in" ell.len1 = "2 in" ell.len2 = "1 in" # Create a single part as solid. part = ell.create(True) #Test() # This needs server minutes to calculate. document.recompute()