Kromě standardních objektových typů jako jsou anotace, sítě a díly, nabízí FreeCAD skvělou možnost vytváření objektů 100% vytvořených skritpy Pythonu, které se nazývají Pythonovské objekty. Tyto objekty se chovají stejně jako jiné objekty FreeCADu a jsou ukládány a načítány automaticky při ukládání a otevírání souboru.
Je třeba pochopit jednu zvláštnost, tyto objekty jsou ukládány ve FcStd souborech FreeCADu s pythonovským modulem json. Tento modul převede pythonovský objekt do řetězce, který je pak možno uložit v souboru. Při načítání naopak tento modul použije uložený řetězec ke znovuvytvoření původního objektu, při tom musí mít přístup ke zdrojovému kódu, který vytvoří objekt. To znamená, že když uložíte takový uživatelský objekt a pak jej otevíráte na počítači kde není pythonovský kód, tak nebude tento objekt vytvořen. Když tedy distribuujete takový objekt někomu jinému, musíte společně s ním distribuovat i pythonovský skript, který objekt vytváří.
Pythonovský objekt má stejné pravidlo jako FreeCAD: Aplikace a GUI jsou odděleny do samostatných částí. Aplikační část, Document Object, definuje konstrukci objektu, zatímco část GUI, View Provider Object, definuje jak bude objekt zobrazen na displeji. View Provider Object, stejně jako další GUI objekty FreeCADu je dostupný pouze když FreeCAD běží se svým vlastním GUI. Pro vytvoření objektu je použitelných několik vlastností a metod. Vlastnosti musejí být některé z předdefinovaných typových vlastností, které nabízí FreeCAD a zobrazují se v dialogovém okně vlastností, takže mohou být uživatelem upravovány. Tímto způsobem jsou Pythonovské objekty správně a zcela parametrizovány. Můžete samostatně definovat vlastnosti objektu a jeho zobrazovacího objektu.
Informace: Ve starších verzích jsme používali pythonovský modul cPickle. Nicméně tento modul spouští libovolný kód a to může být bezpečnostní problém. Proto jsme přešli k pythonovskému modulu json.
The following sample can be found in the src/Mod/TemplatePyMod/FeaturePython.py file, together with several other examples:
'''Examples for a feature class and its view provider.''' import FreeCAD, FreeCADGui from pivy import coin class Box: def __init__(self, obj): '''Add some custom properties to our box feature''' obj.addProperty("App::PropertyLength","Length","Box","Length of the box").Length=1.0 obj.addProperty("App::PropertyLength","Width","Box","Width of the box").Width=1.0 obj.addProperty("App::PropertyLength","Height","Box", "Height of the box").Height=1.0 obj.Proxy = self def onChanged(self, fp, prop): '''Do something when a property has changed''' FreeCAD.Console.PrintMessage("Change property: " + str(prop) + "\n") def execute(self, fp): '''Do something when doing a recomputation, this method is mandatory''' FreeCAD.Console.PrintMessage("Recompute Python Box feature\n") class ViewProviderBox: def __init__(self, obj): '''Set this object to the proxy object of the actual view provider''' obj.addProperty("App::PropertyColor","Color","Box","Color of the box").Color=(1.0,0.0,0.0) obj.Proxy = self def attach(self, obj): '''Setup the scene sub-graph of the view provider, this method is mandatory''' self.shaded = coin.SoGroup() self.wireframe = coin.SoGroup() self.scale = coin.SoScale() self.color = coin.SoBaseColor() data=coin.SoCube() self.shaded.addChild(self.scale) self.shaded.addChild(self.color) self.shaded.addChild(data) obj.addDisplayMode(self.shaded,"Shaded"); style=coin.SoDrawStyle() style.style = coin.SoDrawStyle.LINES self.wireframe.addChild(style) self.wireframe.addChild(self.scale) self.wireframe.addChild(self.color) self.wireframe.addChild(data) obj.addDisplayMode(self.wireframe,"Wireframe"); self.onChanged(obj,"Color") def updateData(self, fp, prop): '''If a property of the handled feature has changed we have the chance to handle this here''' # fp is the handled feature, prop is the name of the property that has changed l = fp.getPropertyByName("Length") w = fp.getPropertyByName("Width") h = fp.getPropertyByName("Height") self.scale.scaleFactor.setValue(float(l),float(w),float(h)) pass def getDisplayModes(self,obj): '''Return a list of display modes.''' modes=[] modes.append("Shaded") modes.append("Wireframe") return modes def getDefaultDisplayMode(self): '''Return the name of the default display mode. It must be defined in getDisplayModes.''' return "Shaded" def setDisplayMode(self,mode): '''Map the display mode defined in attach with those defined in getDisplayModes.\ Since they have the same names nothing needs to be done. This method is optional''' return mode def onChanged(self, vp, prop): '''Here we can do something when a single property got changed''' FreeCAD.Console.PrintMessage("Change property: " + str(prop) + "\n") if prop == "Color": c = vp.getPropertyByName("Color") self.color.rgb.setValue(c[0],c[1],c[2]) def getIcon(self): '''Return the icon in XPM format which will appear in the tree view. This method is\ optional and if not defined a default icon is shown.''' return """ /* XPM */ static const char * ViewProviderBox_xpm[] = { "16 16 6 1", " c None", ". c #141010", "+ c #615BD2", "@ c #C39D55", "# c #000000", "$ c #57C355", " ........", " ......++..+..", " .@@@@.++..++.", " .@@@@.++..++.", " .@@ .++++++.", " ..@@ .++..++.", "###@@@@ .++..++.", "##$.@@$#.++++++.", "#$#$.$$$........", "#$$####### ", "#$$#$$$$$# ", "#$$#$$$$$# ", "#$$#$$$$$# ", " #$#$$$$$# ", " ##$$$$$# ", " ####### "}; """ def __getstate__(self): '''When saving the document this object gets stored using Python's json module.\ Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\ to return a tuple of all serializable objects or None.''' return None def __setstate__(self,state): '''When restoring the serialized object from document we have the chance to set some internals here.\ Since no data were serialized nothing needs to be done here.''' return None def makeBox(): FreeCAD.newDocument() a=FreeCAD.ActiveDocument.addObject("App::FeaturePython","Box") Box(a) ViewProviderBox(a.ViewObject) makeBox()
Vlastnosti jsou skutečné základní kameny pythonovských objektů. Jejich prostřednictvím je uživatel schopen pracovat s objektem. Po vytvoření Pythonovského objektu v dokumentu ( obj=FreeCAD.ActiveDocument.addObject("App::FeaturePython","Box") ), obdržíte seznam dostupných vlastností zadáním:
obj.supportedProperties()
Dostanete seznam dostupných vlastností:
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
Když do uživatelského objektu přidáváte vlastnosti dejte pozor na::
Standardně mohou být vlastnosti upravovány. Je ale možné nastavit vlastnosti pouze ke čtení, třeba když má jenom zobrazovat výstup výsledku metody. Je možné také vlastnost skrýt. Typ vlastnosti může být nastaven použitím
obj.setEditorMode("MyPropertyName", mode)
kde mode je malá celočíselná hodnota, které může být nastavena na:
0 -- defaultní mód, čtení i zápis 1 -- pouze čtení 2 -- skryto
The EditorModes are not set at FreeCAD file reload. This could to be done by the __setstate__ function. See http://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=10#p108072. By using the setEditorMode the properties are only read only in PropertyEditor. They could still be changed from python. To really make them read only the setting has to be passed directly inside the addProperty function. See http://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=20#p109709 for an example.
Tento příklad používá Modul Díl k vytvoření osmistěnu a potom vytvoří pomocí Pivy jeho reprezentaci v Coinu.
První je samotné vytvoření dokumentu:
import FreeCAD, FreeCADGui, Part import pivy from pivy import coin class Octahedron: def __init__(self, obj): "Add some custom properties to our box feature" obj.addProperty("App::PropertyLength","Length","Octahedron","Length of the octahedron").Length=1.0 obj.addProperty("App::PropertyLength","Width","Octahedron","Width of the octahedron").Width=1.0 obj.addProperty("App::PropertyLength","Height","Octahedron", "Height of the octahedron").Height=1.0 obj.addProperty("Part::PropertyPartShape","Shape","Octahedron", "Shape of the octahedron") obj.Proxy = self def execute(self, fp): # Define six vetices for the shape v1 = FreeCAD.Vector(0,0,0) v2 = FreeCAD.Vector(fp.Length,0,0) v3 = FreeCAD.Vector(0,fp.Width,0) v4 = FreeCAD.Vector(fp.Length,fp.Width,0) v5 = FreeCAD.Vector(fp.Length/2,fp.Width/2,fp.Height/2) v6 = FreeCAD.Vector(fp.Length/2,fp.Width/2,-fp.Height/2) # Make the wires/faces f1 = self.make_face(v1,v2,v5) f2 = self.make_face(v2,v4,v5) f3 = self.make_face(v4,v3,v5) f4 = self.make_face(v3,v1,v5) f5 = self.make_face(v2,v1,v6) f6 = self.make_face(v4,v2,v6) f7 = self.make_face(v3,v4,v6) f8 = self.make_face(v1,v3,v6) shell=Part.makeShell([f1,f2,f3,f4,f5,f6,f7,f8]) solid=Part.makeSolid(shell) fp.Shape = solid # helper mehod to create the faces def make_face(self,v1,v2,v3): wire = Part.makePolygon([v1,v2,v3,v1]) face = Part.Face(wire) return face
Pak máme objekt pro zobrazení (view provider object), zodpovědný za zobrazení objektu ve 3D:
class ViewProviderOctahedron: def __init__(self, obj): "Set this object to the proxy object of the actual view provider" obj.addProperty("App::PropertyColor","Color","Octahedron","Color of the octahedron").Color=(1.0,0.0,0.0) obj.Proxy = self def attach(self, obj): "Setup the scene sub-graph of the view provider, this method is mandatory" self.shaded = coin.SoGroup() self.wireframe = coin.SoGroup() self.scale = coin.SoScale() self.color = coin.SoBaseColor() self.data=coin.SoCoordinate3() self.face=coin.SoIndexedLineSet() self.shaded.addChild(self.scale) self.shaded.addChild(self.color) self.shaded.addChild(self.data) self.shaded.addChild(self.face) obj.addDisplayMode(self.shaded,"Shaded"); style=coin.SoDrawStyle() style.style = coin.SoDrawStyle.LINES self.wireframe.addChild(style) self.wireframe.addChild(self.scale) self.wireframe.addChild(self.color) self.wireframe.addChild(self.data) self.wireframe.addChild(self.face) obj.addDisplayMode(self.wireframe,"Wireframe"); self.onChanged(obj,"Color") def updateData(self, fp, prop): "If a property of the handled feature has changed we have the chance to handle this here" # fp is the handled feature, prop is the name of the property that has changed if prop == "Shape": s = fp.getPropertyByName("Shape") self.data.point.setNum(6) cnt=0 for i in s.Vertexes: self.data.point.set1Value(cnt,i.X,i.Y,i.Z) cnt=cnt+1 self.face.coordIndex.set1Value(0,0) self.face.coordIndex.set1Value(1,1) self.face.coordIndex.set1Value(2,2) self.face.coordIndex.set1Value(3,-1) self.face.coordIndex.set1Value(4,1) self.face.coordIndex.set1Value(5,3) self.face.coordIndex.set1Value(6,2) self.face.coordIndex.set1Value(7,-1) self.face.coordIndex.set1Value(8,3) self.face.coordIndex.set1Value(9,4) self.face.coordIndex.set1Value(10,2) self.face.coordIndex.set1Value(11,-1) self.face.coordIndex.set1Value(12,4) self.face.coordIndex.set1Value(13,0) self.face.coordIndex.set1Value(14,2) self.face.coordIndex.set1Value(15,-1) self.face.coordIndex.set1Value(16,1) self.face.coordIndex.set1Value(17,0) self.face.coordIndex.set1Value(18,5) self.face.coordIndex.set1Value(19,-1) self.face.coordIndex.set1Value(20,3) self.face.coordIndex.set1Value(21,1) self.face.coordIndex.set1Value(22,5) self.face.coordIndex.set1Value(23,-1) self.face.coordIndex.set1Value(24,4) self.face.coordIndex.set1Value(25,3) self.face.coordIndex.set1Value(26,5) self.face.coordIndex.set1Value(27,-1) self.face.coordIndex.set1Value(28,0) self.face.coordIndex.set1Value(29,4) self.face.coordIndex.set1Value(30,5) self.face.coordIndex.set1Value(31,-1) def getDisplayModes(self,obj): "Return a list of display modes." modes=[] modes.append("Shaded") modes.append("Wireframe") return modes def getDefaultDisplayMode(self): "Return the name of the default display mode. It must be defined in getDisplayModes." return "Shaded" def setDisplayMode(self,mode): return mode def onChanged(self, vp, prop): "Here we can do something when a single property got changed" FreeCAD.Console.PrintMessage("Change property: " + str(prop) + "\n") if prop == "Color": c = vp.getPropertyByName("Color") self.color.rgb.setValue(c[0],c[1],c[2]) def getIcon(self): return """ /* XPM */ static const char * ViewProviderBox_xpm[] = { "16 16 6 1", " c None", ". c #141010", "+ c #615BD2", "@ c #C39D55", "# c #000000", "$ c #57C355", " ........", " ......++..+..", " .@@@@.++..++.", " .@@@@.++..++.", " .@@ .++++++.", " ..@@ .++..++.", "###@@@@ .++..++.", "##$.@@$#.++++++.", "#$#$.$$$........", "#$$####### ", "#$$#$$$$$# ", "#$$#$$$$$# ", "#$$#$$$$$# ", " #$#$$$$$# ", " ##$$$$$# ", " ####### "}; """ def __getstate__(self): return None def __setstate__(self,state): return None
A nakonec, když je objekt i jeho zobrazení definováno, stačí ho už jen zavolat:
FreeCAD.newDocument() a=FreeCAD.ActiveDocument.addObject("App::FeaturePython","Octahedron") Octahedron(a) ViewProviderOctahedron(a.ViewObject)
Chcete-li aby bylo možné objekt vybrat nebo alespoň jeho část, kliknutím na něj v pohledu, musíte včlenit jeho Coin konstrukci do uzlu SoFCSelection. Má-li objekt komplexní zobrazení s widgety, anotacemi atd., můžete chtít včlenit do SoFCSelection pouze nějakou část. Všechno co je SoFCSelection je průběžně skenováno FreeCADem pro detekci výběru/předvýběru, takže je rozumné nepřetěžovat jej zbytečným skenováním. Tady je co byste měli zahrnout do self.face z příkladu nahoře.
selectionNode = coin.SoType.fromName("SoFCSelection").createInstance() selectionNode.documentName.setValue(FreeCAD.ActiveDocument.Name) selectionNode.objectName.setValue(obj.Object.Name) # here obj is the ViewObject, we need its associated App Object selectionNode.subElementName.setValue("Face") selectNode.addChild(self.face) ... self.shaded.addChild(selectionNode) self.wireframe.addChild(selectionNode)
Jednoduše vytvoříte uzel SoFCSelection, potom přidáte konstrukční uzly a potom přidáte hlavní uzel misto přímého přidávání konstrukčních uzlů.
Jestliže z parametrického objektu vychází jednoduchý tvar, není nutné používat zobrazovací objekt. Tvar bude zobrazován použitím standardního zobrazování tvarů ve FreeCADu.
import FreeCAD as App import FreeCADGui import FreeCAD import Part class Line: def __init__(self, obj): '''"App two point properties" ''' obj.addProperty("App::PropertyVector","p1","Line","Start point") obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(1,0,0) obj.Proxy = self def execute(self, fp): '''"Print a short message when doing a recomputation, this method is mandatory" ''' fp.Shape = Part.makeLine(fp.p1,fp.p2) a=FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Line") Line(a) a.ViewObject.Proxy=0 # just set it to something different from None (this assignment is needed to run an internal notification) FreeCAD.ActiveDocument.recompute()
Same code with use ViewProviderLine
import FreeCAD as App import FreeCADGui import FreeCAD import Part class Line: def __init__(self, obj): '''"App two point properties" ''' obj.addProperty("App::PropertyVector","p1","Line","Start point") obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(100,0,0) obj.Proxy = self def execute(self, fp): '''"Print a short message when doing a recomputation, this method is mandatory" ''' fp.Shape = Part.makeLine(fp.p1,fp.p2) class ViewProviderLine: def __init__(self, obj): ''' Set this object to the proxy object of the actual view provider ''' obj.Proxy = self def getDefaultDisplayMode(self): ''' Return the name of the default display mode. It must be defined in getDisplayModes. ''' return "Flat Lines" a=FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Line") Line(a) ViewProviderLine(a.ViewObject) App.ActiveDocument.recompute()
There are a few very interesting forum threads about scripted objects:
- http://forum.freecadweb.org/viewtopic.php?f=22&t=13740
- http://forum.freecadweb.org/viewtopic.php?t=12139
- https://forum.freecadweb.org/viewtopic.php?f=18&t=13460&start=20#p109709
- https://forum.freecadweb.org/viewtopic.php?f=22&t=21330
In addition to the examples presented here have a look at FreeCAD source code src/Mod/TemplatePyMod/FeaturePython.py for more examples.