Neben den Standard-Objekttypen wie Beschriftungen, Netze und Bauteilobjekte bietet FreeCAD auch die erstaunliche Möglichkeit, 100% mit Python geschriebene parametrische Objekte zu erstellen, die als Python-Objekt (App FeaturePython-Objekt) bezeichnet werden. Diese Objekte verhalten sich genau wie jedes andere FreeCAD-Objekt und werden beim Speichern/Laden von Dateien automatisch gespeichert und wiederhergestellt.
Eine Besonderheit muss beachtet werden: Aus Sicherheitsgründen enthalten FreeCAD Dateien niemals eingebetteten Code. Der Python-Code, den du zum Erstellen von parametrischen Objekten schreibst, wird niemals innerhalb einer Datei gespeichert. Das bedeutet, dass wenn du eine Datei, die ein solches Objekt enthält, auf einem anderen Rechner öffnest und dieser Python-Code auf dem Rechner nicht vorhanden ist, das Objekt nicht vollständig neu erstellt wird. Wenn du solche Objekte an andere weitergibst, musst du auch dein Python-Skript weitergeben, z. B. als Makro.
Hinweis: Es ist möglich, Python-Code in eine FreeCAD-Datei zu packen, indem man json-Serialisierung mit einem App::PropertyPythonObject verwendet, aber dieser Code kann nie direkt ausgeführt werden und hat daher für unseren Zweck hier wenig Nutzen.
Python-Objekte folgen der gleichen Regel wie alle FreeCAD-Objekte: Sie sind in App- und GUI-Bestandteile getrennt. Der App-Bestandteil, das Dokument-Objekt, definiert die Geometrie unseres Objekts, während sein GUI-Bestandteil, das View-Provider-Objekt, bestimmt, wie das Objekt auf dem Bildschirm dargestellt wird. Das View-Provider-Objekt ist, wie jede andere FreeCAD-Objekt, nur verfügbar, wenn FreeCAD in seiner eigenen GUI ausgeführt wird. Es stehen mehrere Eigenschaften und Methoden zur Verfügung, um ein Objekt zu erstellen. Die Eigenschaften müssen zu einer der vordefinierten Eigenschaftsarten gehören, die FreeCAD bereitstellt und werden im Fenster der Eigenschaften-Ansicht angezeigt, damit sie vom Benutzer bearbeitet werden können. Auf diese Weise sind FeaturePython-Objekte wirklich vollständig parametrisch. Eigenschaften für das Objekt und sein Ansichtsobjekt können getrennt festgelegt werden.
Das folgende Beispiel ist zusammen mit einigen anderen Beispielen in der Datei src/Mod/TemplatePyMod/FeaturePython.py zu finden:
'''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 dumps(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 loads(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()
Ist dein Objekt darauf angewiesen, dass es neu berechnet wird, sobald es erstellt wurde, muss dies manuell in der Funktion __init__ aufgerufen werden, da es nicht automatisch geschieht. In diesem Beispiel ist das nicht erforderlich, da die Methode onChanged der Klasse Box den gleichen Effekt hat wie die Funktion execute; aber die unten folgenden Beispiele sind darauf angewiesen, dass eine Neuberechnung erfolgt, bevor irgend etwas in der 3D-Ansicht angezeigt wird. In den Beispielen wird dies manuell mit ActiveDocument.recompute() durchgeführt, aber in komplexeren Szenarien muss man entscheiden, entweder das gesamte Dokument oder das FeaturePython-Objekt neu zu berechnen.
Dieses Beispiel generiert einige Meldungen des Ausnahmestapels (exception stack) im Ausgabefenster. Dies erfolgt, da die Methode onChanged der Klasse Box jedes Mal aufgerufen wird, wenn eine Eigenschaft in der Methode __init__ hinzugefügt wird. Wenn die erste hinzugefügt wird, gibt es die Eigenschaften Width und Height noch nicht, deshalb schlägt der Versuch auf diese zuzugreifen fehl.
Eine Erklärung zu __getstate__ und __setstate__, die durch dumps und loads ersetzt wurden, befindet sich im Forumsbeitrag obj.Proxy.Type is a dict, not a string.
obj.addProperty(...) gibt obj zurück, sodass der Wert der Eigenschaft in der gleichen Zeile angegeben werden kann:
obj.addProperty("App::PropertyLength", "Length", "Box", "Length of the box").Length = 1.0
Das entspricht:
obj.addProperty("App::PropertyLength", "Length", "Box", "Length of the box")
obj.Length = 1.0
Dieses Beispiel verwendet das Part-Modul, um ein Oktaeder zu erstellen und dann seine Coin-Darstellung mit Pivy.
Zuerst das Dokumentobjekt selbst:
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
Dann haben wir das View-Provider-Objekt, das für die Darstellung des Objekts in der 3D-Szene verantwortlich ist:
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.SoIndexedFaceSet()
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 dumps(self):
return None
def loads(self,state):
return None
Schließlich müssen wir unser Objekt und sein Ansichtsobjekt, sobald sie definiert sind, nur noch aufrufen (Der Code für die Octahedron-Klasse und die View-Provider-Klasse kann direkt in FreeCADs Python-Konsole kopiert werden):
FreeCAD.newDocument()
a=FreeCAD.ActiveDocument.addObject("App::FeaturePython","Octahedron")
Octahedron(a)
ViewProviderOctahedron(a.ViewObject)
Wenn du dein Objekt oder zumindest einen Teil davon durch Anklicken im Ansichtsfenster auswählbar machen möchtest, musst du seine Coin-Geometrie in einen SoFCSelection-Knoten einschließen. Wenn dein Objekt eine komplexe Darstellung hat, mit Widgets, Anmerkungen usw., möchtest du vielleicht nur einen Teil davon in eine SoFCSelection einschließen. Alles, was eine SoFCSelection ist, wird von FreeCAD ständig gescannt, um eine Auswahl bzw. Vorauswahl zu erkennen, daher ist es sinnvoll zu versuchen, es nicht mit unnötigem Scannen zu überlasten.
Sobald sich jene Teile des Szenendiagramms, die auswählbar sein sollen, in den SoFCSelection-Knoten befinden, müssen zwei Methoden bereit gestellt werden, um den Auswahlpfad zu bearbeiten. Der Auswahlpfad kann die Form einer Zeichenfolge mit den Namen jedes Elementes im Pfad, oder einer Anordnung (Array) aus Objekten des Szenendiagramms annehmen. Die beiden Methoden sind getDetailPath, welche einen Zeichenfolge-Pfad in eine Anordnung aus Objekten eines Szenendiagramms umwandelt und getElementPicked, welche ein im Szenendiagramm angeklicktes Element erhält, und dessen Zeichenfolge-Namen zurückgibt (Hinweis, nicht seinen Zeichenfolge-Pfad).
Hier ist das obige Molekülbeispiel, angepasst, um die Elemente des Moleküls auswählbar zu machen:
class Molecule:
def __init__(self, obj):
''' Add two point properties '''
obj.addProperty("App::PropertyVector","p1","Line","Start point")
obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(5,0,0)
obj.Proxy = self
def onChanged(self, fp, prop):
if prop == "p1" or prop == "p2":
''' Print the name of the property that has changed '''
fp.Shape = Part.makeLine(fp.p1,fp.p2)
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 ViewProviderMolecule:
def __init__(self, obj):
''' Set this object to the proxy object of the actual view provider '''
obj.Proxy = self
self.ViewObject = obj
sep1=coin.SoSeparator()
sel1 = coin.SoType.fromName('SoFCSelection').createInstance()
# sel1.policy.setValue(coin.SoSelection.SHIFT)
sel1.ref()
sep1.addChild(sel1)
self.trl1=coin.SoTranslation()
sel1.addChild(self.trl1)
sel1.addChild(coin.SoSphere())
sep2=coin.SoSeparator()
sel2 = coin.SoType.fromName('SoFCSelection').createInstance()
sel2.ref()
sep2.addChild(sel2)
self.trl2=coin.SoTranslation()
sel2.addChild(self.trl2)
sel2.addChild(coin.SoSphere())
obj.RootNode.addChild(sep1)
obj.RootNode.addChild(sep2)
self.updateData(obj.Object, 'p2')
self.sel1 = sel1
self.sel2 = sel2
def getDetailPath(self, subname, path, append):
vobj = self.ViewObject
if append:
path.append(vobj.RootNode)
path.append(vobj.SwitchNode)
mode = vobj.SwitchNode.whichChild.getValue()
if mode >= 0:
mode = vobj.SwitchNode.getChild(mode)
path.append(mode)
sub = Part.splitSubname(subname)[-1]
if sub == 'Atom1':
path.append(self.sel1)
elif sub == 'Atom2':
path.append(self.sel2)
else:
path.append(mode.getChild(0))
return True
def getElementPicked(self, pp):
path = pp.getPath()
if path.findNode(self.sel1) >= 0:
return 'Atom1'
if path.findNode(self.sel2) >= 0:
return 'Atom2'
raise NotImplementedError
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 == "p1":
p = fp.getPropertyByName("p1")
self.trl1.translation=(p.x,p.y,p.z)
elif prop == "p2":
p = fp.getPropertyByName("p2")
self.trl2.translation=(p.x,p.y,p.z)
def dumps(self):
return None
def loads(self,state):
return None
def makeMolecule():
FreeCAD.newDocument()
a=FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Molecule")
Molecule(a)
ViewProviderMolecule(a.ViewObject)
FreeCAD.ActiveDocument.recompute()
Wenn ein parametrisches Objekt einfach nur eine Form ausgibt, muss man keine View-Provider-Objekt verwenden. Die Form wird mit FreeCADs Standard-Form-Darstellung angezeigt.
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()
Gleicher Code unter Verwendung von 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()
Du hast vielleicht bemerkt, dass die obigen Beispiele deine Szenengraphen auf leicht unterschiedliche Weise aufbauen. Einige verwenden obj.addDisplayMode(node, "modename"), während andere obj.SwitchNode.getChild(x).addChild(y) verwenden.
Jedes Feature in einem FreeCAD Dokument basiert auf der folgenden Struktur des Szenegraph:
RootNode
\- SwitchNode
\- Shaded
- Wireframe
- etc
Der SwitchNode zeigt nur eines seiner untergeordneten Elemente an, je nachdem, welcher Anzeigemodus in FreeCAD ausgewählt ist.
Die Beispiele welche addDisplayMode verwenden bilden ihre Szenegraphen ausschließlich aus coin3d Szenegraph Elementen. Im Hintergrund fügt addDisplayMode ein neues Kind zum SwitchNode; der Name dieses Knotens stimmt mit dem durchgeführten display mode überein.
Die Beispiele die SwitchNode.getChild(x).addChild verwenden bilden Teile ihrer Geometrie durch Verwenden von Funktionen des Workbench Part, wie etwa fp.Shape = Part.makeLine(fp.p1,fp.p2). Dies erstellt verschiedene Ausgabemodi des Szenegraph unter dem SwitchNode; wenn wir später coin3d Elemente zum Szenegraph hinzufügen müssen wir sie zum vorhandenen Ausgabemodus des Szenegraphen eher durch Verwenden von addChild als durch Erzeugen eines neuen Kindes des SwitchNode hinzufügen.
Bei Verwendung von addDisplayMode() zum Hinzufügen von Geometrie zum Szenegraph, sollte jeder Ausgabemodus seinen eigenen Knoten haben, der an addDisplayMode() übergeben wird; verwende nicht den Gleichen Knoten dafür. Wenn man das macht wird der Auswahlmechanismus verwirrt. Es ist in Ordnung, wenn jeder Knoten eines Ausgabemodus weiter unten die gleichen Geometrieknoten angefügt hat, nur die Wurzel jedes Ausgabemodus muss eindeutig sein.
Hier das obige Molekül Beispiel, angepasst so dass im Szenegraph nur Coin3D Objekte an Stelle von Objekten des Workbench Part gezeichnet werden:
import Part
from pivy import coin
class Molecule:
def __init__(self, obj):
''' Add two point properties '''
obj.addProperty("App::PropertyVector","p1","Line","Start point")
obj.addProperty("App::PropertyVector","p2","Line","End point").p2=FreeCAD.Vector(5,0,0)
obj.Proxy = self
def onChanged(self, fp, prop):
pass
def execute(self, fp):
''' Print a short message when doing a recomputation, this method is mandatory '''
pass
class ViewProviderMolecule:
def __init__(self, obj):
''' Set this object to the proxy object of the actual view provider '''
self.constructed = False
obj.Proxy = self
self.ViewObject = obj
def attach(self, obj):
material = coin.SoMaterial()
material.diffuseColor = (1.0, 0.0, 0.0)
material.emissiveColor = (1.0, 0.0, 0.0)
drawStyle = coin.SoDrawStyle()
drawStyle.pointSize.setValue(10)
drawStyle.style = coin.SoDrawStyle.LINES
wireframe = coin.SoGroup()
shaded = coin.SoGroup()
self.wireframe = wireframe
self.shaded = shaded
self.coords = coin.SoCoordinate3()
self.coords.point.setValues(0, 2, [FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(1, 0, 0)])
wireframe += self.coords
wireframe += drawStyle
wireframe += material
shaded += self.coords
shaded += drawStyle
shaded += material
g = coin.SoGroup()
sel1 = coin.SoType.fromName('SoFCSelection').createInstance()
sel1.style = 'EMISSIVE_DIFFUSE'
p1 = coin.SoType.fromName('SoIndexedPointSet').createInstance()
p1.coordIndex.set1Value(0, 0)
sel1 += p1
g += sel1
wireframe += g
shaded += g
g = coin.SoGroup()
sel2 = coin.SoType.fromName('SoFCSelection').createInstance()
sel2.style = 'EMISSIVE_DIFFUSE'
p2 = coin.SoType.fromName('SoIndexedPointSet').createInstance()
p2.coordIndex.set1Value(0, 1)
sel2 += p2
g += sel2
wireframe += g
shaded += g
g = coin.SoGroup()
sel3 = coin.SoType.fromName('SoFCSelection').createInstance()
sel3.style = 'EMISSIVE_DIFFUSE'
p3 = coin.SoType.fromName('SoIndexedLineSet').createInstance()
p3.coordIndex.setValues(0, 2, [0, 1])
sel3 += p3
g += sel3
wireframe += g
shaded += g
obj.addDisplayMode(wireframe, 'Wireframe')
obj.addDisplayMode(shaded, 'Shaded')
self.sel1 = sel1
self.sel2 = sel2
self.sel3 = sel3
self.constructed = True
self.updateData(obj.Object, 'p2')
def getDetailPath(self, subname, path, append):
vobj = self.ViewObject
if append:
path.append(vobj.RootNode)
path.append(vobj.SwitchNode)
mode = vobj.SwitchNode.whichChild.getValue()
FreeCAD.Console.PrintWarning("getDetailPath: mode {} is active\n".format(mode))
if mode >= 0:
mode = vobj.SwitchNode.getChild(mode)
path.append(mode)
sub = Part.splitSubname(subname)[-1]
print(sub)
if sub == 'Atom1':
path.append(self.sel1)
elif sub == 'Atom2':
path.append(self.sel2)
elif sub == 'Line':
path.append(self.sel3)
else:
path.append(mode.getChild(0))
return True
def getElementPicked(self, pp):
path = pp.getPath()
if path.findNode(self.sel1) >= 0:
return 'Atom1'
if path.findNode(self.sel2) >= 0:
return 'Atom2'
if path.findNode(self.sel3) >= 0:
return 'Line'
raise NotImplementedError
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 not self.constructed:
return
if prop == "p1":
p = fp.getPropertyByName("p1")
self.coords.point.set1Value(0, p)
elif prop == "p2":
p = fp.getPropertyByName("p2")
self.coords.point.set1Value(1, p)
def getDisplayModes(self, obj):
return ['Wireframe', 'Shaded']
def getDefaultDisplayMode(self):
return 'Shaded'
def setDisplayMode(self, mode):
return mode
def dumps(self):
return None
def loads(self,state):
return None
def makeMolecule():
FreeCAD.newDocument()
a=FreeCAD.ActiveDocument.addObject("App::FeaturePython","Molecule")
Molecule(a)
b=ViewProviderMolecule(a.ViewObject)
a.touch()
FreeCAD.ActiveDocument.recompute()
return a,b
a,b = makeMolecule()
Werden skriptgenerierte Objekte in PartDesign erstellt, dann ist die Vorgehensweise ähnlich den oben besprochenen skriptgenerierten Objekten, aber mit ein paar zusätzlichen Überlegungen. Wir müssen zwei Formeigenschaften verarbeiten, eine für die Form, die wir in der 3D-Ansicht sehen, und eine andere für die Form, die von den musternden Werkzeugen verwendet wird, wie die Formelemente für polarer Muster. Die Objektformen müssen auch mit dem Material, das sich schon im Körper befindet, vereinigt werden (oder im Falle von abziehenden Formelementen daraus herausgeschnitten).
Skriptgenerierte Festkörperobjekte in Part Design sollten eher auf PartDesign::FeaturePython, PartDesign::FeatureAdditivePython, oder PartDesign::FeatureSubtractivePython basieren als auf Part::FeaturePython. Nur hinzufügenden und abziehenden Varianten können in musternden Werkzeugen verwendet werden; und wird ein auf Part::FeaturePython basierendes Objekt vom Anwender auf einen Part Design Körper abgelegt, wird es eher zu einem BaseFeature als dass es als natives PartDesign-Objekt angesehen wird. Hinweis:Von allen wird erwartet, dass sie Festkörper sind; erstellt man etwas anderes als einen Festkörper, sollte es auf einem Part::FeaturePython basieren, sonst versucht das nächste Formelement sich im Modellbaum als Festkörper damit zu vereinigen, was fehlschlagen wird.
Hier ist ein einfaches Beispiel das eine Rohr Grundform erzeugt, ähnlich zur Rohr Grundform im Workbench Part, außer dass ein Part Design Solid Feature Objekt erzeugt wird. Wir verwenden zwei getrennte Dateien: pdtube.FCMacro und pdtube.py. Die .FCMacro Datei wird um das Objekt zu erzeugen vom User ausgeführt. Die .py Datei enthält die Definitionen der Klassen die vom .FCMacro importiert werden. Dies wird so gemacht, um die parametrische Natur des Objektes nach einem Neustart von FreeCAD und Öffnen des Dokumentes das eines unserer Rohre enthält zu unterstützen.
Zuerst die Datei welche die Klassen definiert:
# -*- coding: utf-8 -*-
#classes should go in pdtube.py
import FreeCAD, FreeCADGui, Part
class PDTube:
def __init__(self,obj):
obj.addProperty("App::PropertyLength","Radius1","Tube","Radius1").Radius1 = 5
obj.addProperty("App::PropertyLength","Radius2","Tube","Radius2").Radius2 = 10
obj.addProperty("App::PropertyLength","Height","Tube","Height of tube").Height = 10
self.makeAttachable(obj)
obj.Proxy = self
def makeAttachable(self, obj):
if int(FreeCAD.Version()[1]) >= 19:
obj.addExtension('Part::AttachExtensionPython')
else:
obj.addExtension('Part::AttachExtensionPython', obj)
obj.setEditorMode('Placement', 0) #non-readonly non-hidden
def execute(self,fp):
outer_cylinder = Part.makeCylinder(fp.Radius2, fp.Height)
inner_cylinder = Part.makeCylinder(fp.Radius1, fp.Height)
if fp.Radius1 == fp.Radius2: #just make cylinder
tube_shape = outer_cylinder
elif fp.Radius1 < fp.Radius2:
tube_shape = outer_cylinder.cut(inner_cylinder)
else: #invert rather than error out
tube_shape = inner_cylinder.cut(outer_cylinder)
if not hasattr(fp, "positionBySupport"):
self.makeAttachable(fp)
fp.positionBySupport()
tube_shape.Placement = fp.Placement
#BaseFeature (shape property of type Part::PropertyPartShape) is provided for us
#with the PartDesign::FeaturePython and related classes, but it might be empty
#if our object is the first object in the tree. it's a good idea to check
#for its existence in case we want to make type Part::FeaturePython, which won't have it
if hasattr(fp, "BaseFeature") and fp.BaseFeature != None:
if "Subtractive" in fp.TypeId:
full_shape = fp.BaseFeature.Shape.cut(tube_shape)
else:
full_shape = fp.BaseFeature.Shape.fuse(tube_shape)
full_shape.transformShape(fp.Placement.inverse().toMatrix(), True) #borrowed from gears workbench
fp.Shape = full_shape
else:
fp.Shape = tube_shape
if hasattr(fp,"AddSubShape"): #PartDesign::FeatureAdditivePython and
#PartDesign::FeatureSubtractivePython have this
#property but PartDesign::FeaturePython does not
#It is the shape used for copying in pattern features
#for example in making a polar pattern
tube_shape.transformShape(fp.Placement.inverse().toMatrix(), True)
fp.AddSubShape = tube_shape
class PDTubeVP:
def __init__(self, obj):
'''Set this object to the proxy object of the actual view provider'''
obj.Proxy = self
def attach(self,vobj):
self.vobj = vobj
def updateData(self, fp, prop):
'''If a property of the handled feature has changed we have the chance to handle this here'''
pass
def getDisplayModes(self,obj):
'''Return a list of display modes.'''
modes=[]
modes.append("Flat Lines")
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 "Flat Lines"
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")
pass
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 dumps(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 loads(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
Und jetzt die Makro-Datei zum Erstellen des Objekts:
# -*- coding: utf-8 -*-
#pdtube.FCMacro
import pdtube
#above line needed if the class definitions above are place in another file: PDTube.py
#this is needed if the tube object is to remain parametric after restarting FreeCAD and loading
#a document containing the object
body = FreeCADGui.ActiveDocument.ActiveView.getActiveObject("pdbody")
if not body:
FreeCAD.Console.PrintError("No active body.\n")
else:
from PySide import QtGui
window = FreeCADGui.getMainWindow()
items = ["Additive","Subtractive","Neither additive nor subtractive"]
item,ok =QtGui.QInputDialog.getItem(window,"Select tube type","Select whether you want additive, subtractive, or neither:",items,0,False)
if ok:
if item == items[0]:
className = "PartDesign::FeatureAdditivePython"
elif item == items[1]:
className = "PartDesign::FeatureSubtractivePython"
else:
className = "PartDesign::FeaturePython" #not usable in pattern features, such as polar pattern
tube = FreeCAD.ActiveDocument.addObject(className,"Tube")
pdtube.PDTube(tube)
pdtube.PDTubeVP(tube.ViewObject)
body.addObject(tube) #optionally we can also use body.insertObject() for placing at particular place in tree
Die Objektarten, die mit FreeCAD.ActiveDocument.addObject() erstellt werden können, hängen von den geladenen Modulen ab. Nach dem Laden aller interner Arbeitsbereiche kann man mit FreeCAD.ActiveDocument.supportedTypes() eine vollständige Liste aller Objektarten erhalten. Nur Objektarten, deren Name auf Python endet, können für skriptgenerierte Objekte verwendet werden. Diese sind hier aufgelistet (für FreeCAD v1.0):
App::DocumentObjectGroupPythonApp::FeaturePythonApp::GeometryPythonApp::LinkElementPythonApp::LinkGroupPythonApp::LinkPythonApp::MaterialObjectPythonApp::PlacementPythonFem::ConstraintPythonFem::FeaturePythonFem::FemAnalysisPythonFem::FemMeshObjectPythonFem::FemResultObjectPythonFem::FemSolverObjectPythonMeasure::MeasurePythonMesh::FeaturePythonPart::CustomFeaturePythonPart::FeaturePythonPart::Part2DObjectPythonPartDesign::FeatureAdditivePythonPartDesign::FeatureAddSubPythonPartDesign::FeaturePythonPartDesign::FeatureSubtractivePythonPartDesign::SubShapeBinderPythonPath::FeatureAreaPythonPath::FeatureAreaViewPythonPath::FeatureCompoundPythonPath::FeaturePythonPath::FeatureShapePythonPoints::FeaturePythonSketcher::SketchObjectPythonSpreadsheet::SheetPythonTechDraw::DrawBrokenViewPythonTechDraw::DrawComplexSectionPythonTechDraw::DrawLeaderLinePythonTechDraw::DrawPagePythonTechDraw::DrawRichAnnoPythonTechDraw::DrawTemplatePythonTechDraw::DrawTilePythonTechDraw::DrawTileWeldPythonTechDraw::DrawViewPartPythonTechDraw::DrawViewPythonTechDraw::DrawViewSectionPythonTechDraw::DrawViewSymbolPythonTechDraw::DrawWeldSymbolPython
Siehe FeaturePython Methoden für eine vollständige Referenz.
Eigenschaften sind die wahren Bausteine von FeaturePython-Objekten. Durch sie kann man mit einem Objekt interagieren und es ändern. Nach dem Erstellen eines neuen FeaturePython-Objekts in einem Dokument, kann man eine Liste der vorhandenen Eigenschaften bekommen:
obj = FreeCAD.ActiveDocument.addObject("App::FeaturePython", "Box")
obj.supportedProperties()
Siehe FunktionsPython benutzerdefinierte Eigenschaften für einen Überblick:
Beim Hinzufügen von Eigenschaften zu benutzerdefinierten Objekten bitte folgendes beachten:
< oder > in den Beschreibungen der Eigenschaften verwenden (das würde die XML-Teile in der .FCStd-Datei auseinanderbrechen)Die Eigenschaften sind in der C++ header-Datei PropertyStandard.h festgelegt.
Standardmäßig können Eigenschaften durch den Anwender geändert werden, aber es ist möglich, die Eigenschaften auf schreibgeschützt zu setzen, wenn man zum Beispiel das Ergebnis einer Methode anzeigen möchte. Es ist auch möglich, eine Eigenschaft auszublenden. Diese Attribute können festgelegt werden mit:
obj.setEditorMode("MyPropertyName", mode)
wobei mode diese Werte annehmen kann:
0 -- Standardmodus, zum Lesen und Schreiben 1 -- Schreibgeschützt (nur zum Lesen) 2 -- Ausgeblendet 3 -- Schreibgeschützt und ausgeblendet
Die Attribute können auch durch Verwenden einer Liste von Zeichenketten festgelegt werden, z.B. obj.setEditorMode("Placement", ["ReadOnly", "Hidden"]).
Attribute, die mit setEditorMode gesetzt wurden, können vom Anwender entfernt werden. Siehe Eigenschafteneditor. Man beachte, dass schreibgeschützte Eigenschaften mit Python (-Anweisungen) geändert werden können.
Diese und weitere Attribute können auch direkt mit der Funktion addProperty gesetzt werden. Attribute, die mit dieser Funktion gesetzt wurden, können nicht vom Anwender geändert werden. Eine interessante Möglichkeit ist es, eine Eigenschaft als Ausgabe-Eigenschaft zu kennzeichnen. Auf diese Weise werden Objekte durch FreeCAD nicht als verändert gekennzeichnet, wenn sie bearbeitet werden (dadurch ist eine Neuberechnung nicht erforderlich).
Beispiel einer Ausgabe Property (siehe auch https://forum.freecad.org/viewtopic.php?t=24928):
obj.addProperty("App::PropertyString", "MyCustomProperty", "", "", 8)
Die unten aufgeführten Attribute können mit addProperty gesetzt werden. Mehrere Attribute können durch Hinzufügen von Werten gesetzt werden.
0 -- Prop_None, Kein spezielles Eigenschaftsattribut 1 -- Prop_ReadOnly, Die Eigenschaft ist im Editor schreibgeschützt 2 -- Prop_Transient, Die Eigenschaft wird nicht in der Datei gespeichert 4 -- Prop_Hidden, Eigenschaft erscheint nicht im Editor 8 -- Prop_Output, Geänderte Eigenschaft berührt nicht ihren Eltern-Container 16 -- Prop_NoRecompute, Geänderte Eigenschaft berührt nicht ihren Container zur Neuberechnung 32 -- Prop_NoPersist, Die Eigenschaft wird überhaupt nicht in der Datei gespeichert
Die Attribute der Eigenschaften sind in der PropertyContainer C++ header-Datei festgelegt.
Für Prop_ReadOnly und Prop_Hidden hat die addProperty Funktion ebenfalls Boolsche Argumente:
obj.addProperty("App::PropertyString", "MyCustomProperty", "", "", 0, True, True)
Was gleichwertig ist zu:
obj.addProperty("App::PropertyString", "MyCustomProperty", "", "", 1+4)
introduced in 1.0: Die vollständige Signatur der Funktion ist:
obj.addProperty(type: string, name: string, group="", doc="", attr=0, read_only=False, hidden=False, enum_vals=[])
type: Property type.name: Property name.group: Property subsection (im Eigenschaften-Editor).doc: Tooltip (idem).attr: Attribute, siehe oben.read_only: Siehe oben.hidden: Siehe oben.enum_vals: Enumeration values (Liste von Strings), nur relevant der Typ "App::PropertyEnumeration" ist.
Die Liste der verfügbaren Erweiterungen kann mit grep -RI EXTENSION_PROPERTY_SOURCE_TEMPLATE im Repository des Quellcodes abgerufen werden und ist hier aufgeführt (für FreeCAD v1.1).
Für Objekte:
App::GeoFeatureGroupExtensionPythonApp::GroupExtensionPythonApp::LinkBaseExtensionPythonApp::LinkExtensionPythonApp::OriginGroupExtensionPythonApp::SuppressibleExtensionPythonPart::AttachExtensionPythonTechDraw::CosmeticExtensionPythonFür Ansichts-Objekte:
Gui::ViewProviderExtensionPythonGui::ViewProviderGeoFeatureGroupExtensionPythonGui::ViewProviderGroupExtensionPythonGui::ViewProviderOriginGroupExtensionPythonGui::ViewProviderSuppressibleExtensionPythonPartGui::ViewProviderAttachExtensionPythonPartGui::ViewProviderGridExtensionPythonPartGui::ViewProviderSplineExtensionPythonEs gibt noch andere Erweiterungen, aber sie funktionieren nicht ohne Weiteres:
App::ExtensionPythonPart::PreviewExtensionPythonPartGui::ViewProviderPreviewExtensionPythonTechDrawGui::ViewProviderCosmeticExtensionPythonTechDrawGui::ViewProviderDrawingViewExtensionPythonTechDrawGui::ViewProviderPageExtensionPythonTechDrawGui::ViewProviderTemplateExtensionPython
Weitere Seiten:
Interessante Forenbeiträge über geskriptete Objekte:
Zusätzlich zu den hier vorgestellten Beispielen solltedz du einen Blick in den FreeCAD Quellcode src/Mod/TemplatePyMod/FeaturePython.py für weitere Beispiele werfen.