# -*- coding: utf-8 -*- __title__ = "Freehand BSpline" __author__ = "Christophe Grellier (Chris_G)" __license__ = "LGPL 2.1" __doc__ = "Creates an freehand BSpline curve" import sys if sys.version_info.major >= 3: from importlib import reload import os import FreeCAD import FreeCADGui import Part from freecad.Curves import _utils from freecad.Curves import profile_editor reload(profile_editor) from freecad.Curves import ICONPATH TOOL_ICON = os.path.join( ICONPATH, 'editableSpline.svg') #debug = _utils.debug #debug = _utils.doNothing '''Available properties : 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 midpoint(e): p = e.FirstParameter + 0.5 * (e.LastParameter - e.FirstParameter) return(e.valueAt(p)) class GordonProfileFP: """Creates an editable interpolation curve""" def __init__(self, obj, s, d, t): """Add the properties""" obj.addProperty("App::PropertyLinkSubList", "Support", "Profile", "Constraint shapes").Support = s obj.addProperty("App::PropertyFloatConstraint","Parametrization", "Profile", "Parametrization factor") obj.addProperty("App::PropertyFloat", "Tolerance", "Profile", "Tolerance").Tolerance = 1e-5 obj.addProperty("App::PropertyBool", "Periodic", "Profile", "Periodic curve").Periodic = False obj.addProperty("App::PropertyVectorList", "Data", "Profile", "Data list").Data = d obj.addProperty("App::PropertyVectorList", "Tangents", "Profile", "Tangents list") obj.addProperty("App::PropertyBoolList", "Flags", "Profile", "Tangent flags") obj.addProperty("App::PropertyIntegerList", "DataType", "Profile", "Types of interpolated points").DataType = t obj.addProperty("App::PropertyBoolList", "LinearSegments", "Profile", "Linear segment flags") obj.Parametrization = ( 1.0, 0.0, 1.0, 0.05 ) obj.Proxy = self def get_shapes(self, fp): if hasattr(fp,'Support'): sl = list() for ob,names in fp.Support: for name in names: if ("Vertex" in name): n = eval(name.lstrip("Vertex")) if len(ob.Shape.Vertexes) >= n: sl.append(ob.Shape.Vertexes[n-1]) elif ("Edge" in name): n = eval(name.lstrip("Edge")) if len(ob.Shape.Edges) >= n: sl.append(ob.Shape.Edges[n-1]) elif ("Face" in name): n = eval(name.lstrip("Face")) if len(ob.Shape.Faces) >= n: sl.append(ob.Shape.Faces[n-1]) return(sl) def get_points(self, fp, stretch=True): touched = False shapes = self.get_shapes(fp) if not len(fp.Data) == len(fp.DataType): FreeCAD.Console.PrintError("Gordon Profile : Data and DataType mismatch\n") return(None) pts = list() shape_idx = 0 for i in range(len(fp.Data)): if fp.DataType[i] == 0: # Free point pts.append(fp.Data[i]) elif (fp.DataType[i] == 1): if (shape_idx < len(shapes)): # project on shape d,p,i = Part.Vertex(fp.Data[i]).distToShape(shapes[shape_idx]) if d > fp.Tolerance: touched = True pts.append(p[0][1]) #shapes[shape_idx].valueAt(fp.Data[i].x)) shape_idx += 1 else: pts.append(fp.Data[i]) if stretch and touched: params = [0] knots = [0] moves = [pts[0]-fp.Data[0]] lsum = 0 mults = [2] for i in range(1,len(pts)): lsum += fp.Data[i-1].distanceToPoint(fp.Data[i]) params.append(lsum) if fp.DataType[i] == 1: knots.append(lsum) moves.append(pts[i]-fp.Data[i]) mults.insert(1,1) mults[-1] = 2 if len(moves) < 2: return(pts) #FreeCAD.Console.PrintMessage("%s\n%s\n%s\n"%(moves,mults,knots)) curve = Part.BSplineCurve() curve.buildFromPolesMultsKnots(moves,mults,knots,False,1) for i in range(1,len(pts)): if fp.DataType[i] == 0: #FreeCAD.Console.PrintMessage("Stretch %s #%d: %s to %s\n"%(fp.Label,i,pts[i],curve.value(params[i]))) pts[i] += curve.value(params[i]) if touched: return(pts) else: return(False) def execute(self, obj): try: o = FreeCADGui.ActiveDocument.getInEdit().Object if o == obj: return except: FreeCAD.Console.PrintWarning("execute is disabled during editing\n") pts = self.get_points(obj) if pts: if len(pts) < 2: FreeCAD.Console.PrintError("%s : Not enough points\n"%obj.Label) return(False) else: obj.Data = pts else: pts = obj.Data tans = [FreeCAD.Vector()]*len(pts) flags = [False]*len(pts) for i in range(len(obj.Tangents)): tans[i] = obj.Tangents[i] for i in range(len(obj.Flags)): flags[i] = obj.Flags[i] #if not (len(obj.LinearSegments) == len(pts)-1): #FreeCAD.Console.PrintError("%s : Points and LinearSegments mismatch\n"%obj.Label) if len(obj.LinearSegments) > 0: for i,b in enumerate(obj.LinearSegments): if b: tans[i] = pts[i+1]-pts[i] tans[i+1] = tans[i] flags[i] = True flags[i+1] = True params = profile_editor.parameterization(pts,obj.Parametrization,obj.Periodic) curve = Part.BSplineCurve() curve.interpolate(Points=pts, Parameters=params, PeriodicFlag=obj.Periodic, Tolerance=obj.Tolerance, Tangents=tans, TangentFlags=flags) obj.Shape = curve.toShape() def onChanged(self, fp, prop): if prop in ("Support","Data","DataType","Periodic"): #FreeCAD.Console.PrintMessage("%s : %s changed\n"%(fp.Label,prop)) if (len(fp.Data)==len(fp.DataType)) and (sum(fp.DataType)==len(fp.Support)): new_pts = self.get_points(fp, True) if new_pts: fp.Data = new_pts if prop == "Parametrization": self.execute(fp) def onDocumentRestored(self, fp): fp.setEditorMode("Data", 2) fp.setEditorMode("DataType", 2) class GordonProfileVP: def __init__(self,vobj): vobj.Proxy = self self.select_state = True self.active = False def getIcon(self): return(TOOL_ICON) def attach(self, vobj): self.Object = vobj.Object self.active = False self.select_state = vobj.Selectable self.ip = None def setEdit(self,vobj,mode=0): if mode == 0: reload(profile_editor) if vobj.Selectable: self.select_state = True vobj.Selectable = False pts = list() sl = list() for ob,names in self.Object.Support: for name in names: sl.append((ob,(name,))) shape_idx = 0 for i in range(len(self.Object.Data)): p = self.Object.Data[i] t = self.Object.DataType[i] if t == 0: pts.append(profile_editor.MarkerOnShape([p])) elif t == 1: pts.append(profile_editor.MarkerOnShape([p],sl[shape_idx])) shape_idx += 1 for i in range(len(pts)): #p,t,f in zip(pts, self.Object.Tangents, self.Object.Flags): if i < min(len(self.Object.Flags),len(self.Object.Tangents)): if self.Object.Flags[i]: pts[i].tangent = self.Object.Tangents[i] self.ip = profile_editor.InterpoCurveEditor(pts, self.Object) self.ip.periodic = self.Object.Periodic self.ip.param_factor = self.Object.Parametrization for i in range(min(len(self.Object.LinearSegments),len(self.ip.lines))): self.ip.lines[i].tangent = self.Object.LinearSegments[i] self.ip.lines[i].updateLine() self.active = True return(True) return(False) def unsetEdit(self,vobj,mode=0): if isinstance(self.ip,profile_editor.InterpoCurveEditor): pts = list() typ = list() tans = list() flags = list() #original_links = self.Object.Support new_links = list() for p in self.ip.points: if isinstance(p,profile_editor.MarkerOnShape): pt = p.points[0] pts.append(FreeCAD.Vector(pt[0],pt[1],pt[2])) if p.sublink: new_links.append(p.sublink) typ.append(1) else: typ.append(0) if p.tangent: tans.append(p.tangent) flags.append(True) else: tans.append(FreeCAD.Vector()) flags.append(False) self.Object.Tangents = tans self.Object.Flags = flags self.Object.LinearSegments = [l.linear for l in self.ip.lines] self.Object.DataType = typ self.Object.Data = pts self.Object.Support = new_links vobj.Selectable = self.select_state self.ip.quit() self.ip = None self.active = False return(True) def doubleClicked(self,vobj): if not hasattr(self,'active'): self.active = False if not self.active: self.active = True #self.setEdit(vobj) vobj.Document.setEdit(vobj) else: vobj.Document.resetEdit() self.active = False return(True) def __getstate__(self): return({"name": self.Object.Name}) def __setstate__(self,state): self.Object = FreeCAD.ActiveDocument.getObject(state["name"]) return(None) class GordonProfileCommand: """Creates a editable interpolation curve""" docu = """*** Interpolation curve control keys :\n a - Select all / Deselect i - Insert point in selected segments t - Set / unset tangent (view direction) p - Align selected objects s - Snap points on shape / Unsnap l - Set/unset a linear interpolation x,y,z - Axis constraints during grab q - Apply changes and quit editing\n""" def makeFeature(self, sub, pts, typ): fp = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Freehand BSpline") proxy = GordonProfileFP(fp,sub,pts,typ) GordonProfileVP(fp.ViewObject) FreeCAD.Console.PrintMessage(GordonProfileCommand.docu) FreeCAD.ActiveDocument.recompute() FreeCADGui.SendMsgToActiveView("ViewFit") fp.ViewObject.Document.setEdit(fp.ViewObject) def Activated(self): s = FreeCADGui.Selection.getSelectionEx() try: ordered = FreeCADGui.activeWorkbench().Selection if ordered: s = ordered except AttributeError: pass sub = list() pts = list() for obj in s: if obj.HasSubObjects: #FreeCAD.Console.PrintMessage("object has subobjects %s\n"%str(obj.SubElementNames)) for n in obj.SubElementNames: sub.append((obj.Object,[n])) for p in obj.PickedPoints: pts.append(p) if len(pts) == 0: pts = [FreeCAD.Vector(0,0,0),FreeCAD.Vector(0.5,0,0),FreeCAD.Vector(1,0,0)] typ = [0,0,0] elif len(pts) == 1: pts.append(pts[0]+FreeCAD.Vector(0.5,0,0)) pts.append(pts[0]+FreeCAD.Vector(1,0,0)) typ = [1,0,0] else: typ = [1]*len(pts) self.makeFeature(sub,pts,typ) def IsActive(self): if FreeCAD.ActiveDocument: return(True) else: return(False) def GetResources(self): return {'Pixmap' : TOOL_ICON, 'MenuText': __title__, 'ToolTip': __doc__} FreeCADGui.addCommand('gordon_profile', GordonProfileCommand())