# -*- coding: utf-8 -*- __title__ = "Helical Sweep" __author__ = "Christophe Grellier (Chris_G)" __license__ = "LGPL 2.1" __doc__ = """Sweep an open wire along an helical path""" import sys if sys.version_info.major >= 3: from importlib import reload import os import FreeCAD import FreeCADGui import Part from math import pi from freecad.Curves import _utils from freecad.Curves import nurbs_tools vec2 = FreeCAD.Base.Vector2d from freecad.Curves import ICONPATH TOOL_ICON = os.path.join( ICONPATH, 'helical_sweep.svg') #debug = _utils.debug #debug = _utils.doNothing props = """ App::PropertyBool App::PropertyBoolList App::PropertyFloat App::PropertyFloatList App::PropertyFloatConstraint App::PropertyQuantity App::PropertyQuantityConstraint App::PropertyAngle App::PropertyDistance App::PropertyLength App::PropertySpeed App::PropertyAcceleration App::PropertyForce App::PropertyPressure App::PropertyInteger App::PropertyIntegerConstraint App::PropertyPercent App::PropertyEnumeration App::PropertyIntegerList App::PropertyIntegerSet App::PropertyMap App::PropertyString App::PropertyUUID App::PropertyFont App::PropertyStringList App::PropertyLink App::PropertyLinkSub App::PropertyLinkList App::PropertyLinkSubList App::PropertyMatrix App::PropertyVector App::PropertyVectorList App::PropertyPlacement App::PropertyPlacementLink App::PropertyColor App::PropertyColorList App::PropertyMaterial App::PropertyPath App::PropertyFile App::PropertyFileIncluded App::PropertyPythonObject Part::PropertyPartShape Part::PropertyGeometryList Part::PropertyShapeHistory Part::PropertyFilletEdges Sketcher::PropertyConstraintList """ def vadd(v1,v2): return vec2(v1.x + v2.x, v1.y + v2.y) def vmul(v1,f): return vec2(v1.x*f, v1.y*f) class HelicalSweep: """Sweep a profile along an helical path""" def __init__(self): self._plane = Part.Plane() self.nb_of_turns = 2.5 self.lead = 10.0 self.cylinder = Part.Cylinder() self.cylinder.transform(FreeCAD.Placement(FreeCAD.Vector(),FreeCAD.Vector(1,0,0),-90).toMatrix()) self.rational_approx = True self.check_splines = False self.max_radius = 0 self._placement = FreeCAD.Placement() def set_placement(self, pl): self._placement = pl self._plane.transform(pl.toMatrix()) self.cylinder.transform(pl.toMatrix()) def sweep_Point2D(self, v, extend=False): offset = 0 nb_turns = self.nb_of_turns if extend: offset = -self.lead*2 nb_turns = self.nb_of_turns + 4 sp = vec2(0, v.y + offset) self.cylinder.Radius = v.x if v.x > self.max_radius: self.max_radius = v.x vector_one_turn = vec2(2*pi,self.lead) vector_full_turns = vmul(vector_one_turn, nb_turns) lineseg = Part.Geom2d.Line2dSegment(sp, vadd(sp,vector_full_turns)) return lineseg.toShape(self.cylinder) def sweep_edge(self, e, extend=False): if self.rational_approx: approx = e.toNurbs().Edges[0] else: approx = e.Curve.toBSpline(e.FirstParameter,e.LastParameter).toShape() bs = approx.Curve #self.cylinder.transform(self.placement.toMatrix()) poles = [] weights = [] oc = False for w,p in zip(bs.getWeights(), bs.getPoles()): p2d = vec2(*self._plane.projectPoint(p,"LowerDistanceParameters")) c = self.sweep_Point2D(p2d, extend).Curve if oc and self.check_splines: nurbs_tools.is_same(oc, c, 1e-3, True) oc = c poles.append(oc.getPoles()) weights.append([w]*oc.NbPoles) bss = Part.BSplineSurface() bss.buildFromPolesMultsKnots(poles, bs.getMultiplicities(),oc.getMultiplicities(), bs.getKnots(), oc.getKnots(), bs.isPeriodic(), oc.isPeriodic(), bs.Degree, oc.Degree, weights) return bss.toShape() def sweep_wire(self, w, solid=False): faces = [] for e in w.Edges: faces.append(self.sweep_edge(e,solid)) shell = Part.Shell(faces) shell.sewShape() if solid: cyl = Part.makeCylinder(self.max_radius*2, self.nb_of_turns*self.lead) cyl.Placement = self._placement.multiply(FreeCAD.Placement(FreeCAD.Vector(),FreeCAD.Vector(1,0,0),-90)) common = cyl.common(shell) cut_faces = common.Faces new_edges = [] for e1 in common.Edges: found = False for e2 in shell.Edges: if nurbs_tools.is_same(e1.Curve, e2.Curve, tol=1e-7, full=False): found = True #print("found similar edges") continue if not found: new_edges.append(e1) #print(len(Part.sortEdges(new_edges))) el1, el2 = Part.sortEdges(new_edges)[0:2] f1 = Part.makeFace(Part.Wire(el1),'Part::FaceMakerSimple') f2 = Part.makeFace(Part.Wire(el2),'Part::FaceMakerSimple') cut_faces.extend([f1,f2]) try: shell = Part.Shell(cut_faces) shell.sewShape() return Part.Solid(shell) except Part.OCCError: print("Failed to create solid") return Part.Compound(cut_faces) return shell class HelicalSweepFP: """Creates a ...""" def __init__(self, obj): """Add the properties""" obj.addProperty("App::PropertyLink", "Profile", "Profile object") obj.addProperty("App::PropertyFloat", "Turns", "Settings", "Number of turns").Turns = 1.0 obj.addProperty("App::PropertyFloat", "Lead", "Settings", "Thread lead (-1 for auto)").Lead = -1 obj.addProperty("App::PropertyBool", "Rational", "Settings", "Allow rational bsplines").Rational = False obj.addProperty("App::PropertyBool", "Solid", "Settings", "Create a solid shape").Solid = False obj.Proxy = self def execute(self, obj): edges = obj.Profile.Shape.Edges hs = HelicalSweep() hs.rational_approx = obj.Rational gpl = obj.Profile.getGlobalPlacement() hs.set_placement(gpl) # 3 priorities to get Lead value # 1 : obj.Lead property is >= 0 # 2 : Sketch has a constraint called "Lead" # 3 : the longest of the DistanceY constraints if hasattr(obj,"Lead") and obj.Lead >= 0: hs.lead = obj.Lead else: dmin = 0 for c in obj.Profile.Constraints: if c.Name == "Lead": dmin = c.Value continue else: if c.Type == "DistanceY": if c.Value > dmin: dmin = c.Value hs.lead = dmin hs.nb_of_turns = obj.Turns obj.Shape = hs.sweep_wire(Part.Wire(edges), obj.Solid) #faces = [hs.sweep_edge(e, obj.Solid) for e in edges] #shell = Part.Shell(faces) #shell.sewShape() #obj.Shape = shell def onChanged(self, obj, prop): if prop == "Lead": if obj.Lead < 0: obj.Lead = -1 class HelicalSweepVP: def __init__(self,vobj): vobj.Proxy = self def getIcon(self): return TOOL_ICON def attach(self, vobj): self.Object = vobj.Object def claimChildren(self): return [self.Object.Profile] def __getstate__(self): return {"name": self.Object.Name} def __setstate__(self,state): self.Object = FreeCAD.ActiveDocument.getObject(state["name"]) return None class HelicalSweepCommand: """Creates a HelicalSweep Object""" def make_profile_sketch(self): import Sketcher sk = FreeCAD.ActiveDocument.addObject('Sketcher::SketchObject','Profile') sk.Placement = FreeCAD.Placement(FreeCAD.Vector(0,0,0),FreeCAD.Rotation(0,0,0,1)) sk.MapMode = "Deactivated" sk.addGeometry(Part.LineSegment(FreeCAD.Vector(100.0,0.0,0),FreeCAD.Vector(127.0,12.0,0)),False) sk.addConstraint(Sketcher.Constraint('PointOnObject',0,1,-1)) sk.addGeometry(Part.ArcOfCircle(Part.Circle(FreeCAD.Vector(125.0,17.0,0),FreeCAD.Vector(0,0,1),5.8),-1.156090,1.050925),False) sk.addConstraint(Sketcher.Constraint('Tangent',0,2,1,1)) sk.addGeometry(Part.LineSegment(FreeCAD.Vector(128.0,22.0,0),FreeCAD.Vector(100.0,37.0,0)),False) sk.addConstraint(Sketcher.Constraint('Tangent',1,2,2,1)) sk.addConstraint(Sketcher.Constraint('Vertical',0,1,2,2)) sk.addConstraint(Sketcher.Constraint('DistanceY',0,1,2,2,37.5)) sk.setDatum(4,FreeCAD.Units.Quantity('35.000000 mm')) sk.renameConstraint(4, u'Lead') sk.setDriving(4,False) sk.addConstraint(Sketcher.Constraint('Equal',2,0)) FreeCAD.ActiveDocument.recompute() return sk def makeFeature(self, sel): fp = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Helical Sweep") HelicalSweepFP(fp) fp.Profile = sel HelicalSweepVP(fp.ViewObject) FreeCAD.ActiveDocument.recompute() def Activated(self): sel = FreeCADGui.Selection.getSelection() if sel == []: sel.append(self.make_profile_sketch()) self.makeFeature(sel[0]) def IsActive(self): if FreeCAD.ActiveDocument: return True else: return False def GetResources(self): return {'Pixmap' : TOOL_ICON, 'MenuText': __title__, 'ToolTip': __doc__} FreeCADGui.addCommand('HelicalSweep', HelicalSweepCommand())