Tutorial KinematicController/de: Difference between revisions

From FreeCAD Documentation
(Created page with "Diese Anleitung beschreibt, wie man eine einfache Kinematiksteuerung aus ein paar Zeilen Python-Kode erstellt, die mit Baugruppen des Arbeitsbereiches Assembly3_Workbench|As...")
(Updating to match new version of source page)
 
(118 intermediate revisions by 2 users not shown)
Line 4: Line 4:
|Topic=Kinematisteuerung erstellt mit Python
|Topic=Kinematisteuerung erstellt mit Python
|Level=Pythongrundkentnisse sind hilfreich
|Level=Pythongrundkentnisse sind hilfreich
|FCVersion=0.20. und später
|FCVersion=0.20 und später
|Time=1 Stunde
|Time=1 Stunde
|Author=[[User:FBXL5|FBXL5]]
|Author=[[User:FBXL5|FBXL5]]
}}
}}


<span id="Introduction"></span>
==Einleitung==
==Einleitung==


Diese Anleitung beschreibt, wie man eine einfache Kinematiksteuerung aus ein paar Zeilen Python-Kode erstellt, die mit Baugruppen des Arbeitsbereiches [[Assembly3_Workbench|Assembly3]] verwendet werden kann.
Diese Anleitung beschreibt, wie man eine einfache Kinematiksteuerung aus ein paar Zeilen Python-Kode erstellt, die mit Baugruppen des Arbeitsbereiches [[Assembly3_Workbench|Assembly3]] verwendet werden kann.


Jeder Texteditor kann zum Erstellen von Kode genutzt werden. Mein Favorit ist Atom, aber FreeCADs eingebauter Editor funktioniert auch bestens.
Any text editor can be used to code. My choice is Atom, but FreeCAD's built-in editor works well, too.


The following code examples can be copied and pasted into an empty text file and then saved under a name of your choice as a {{FileName|*.py}} or {{FileName|*.FCMacro}} file.
Die folgenden Kode-Beispiele können kopiert, in eine leere Textdatei eingefügt und dann unter einem selbstgewählten Namen als {{FileName|*.py}} oder *{{FileName|*.FCMacro}} abgespeichert werden.


<span id="Macro_sections"></span>
==Macro sections==
==Makroabschnitte==


<span id="Basic_structure"></span>
===Basic structure===
===Grundstruktur===


{{Code|code=
{{Code|code=
Line 35: Line 38:
}}
}}


The basic structure consists of a {{Incode|main()}} function and a switch to check if the macro is used as a container for classes, methods etc. or if it is run on its own. Only the second option will start the {{Incode|main()}} function. The function is empty for now.
Die Grundstruktur besteht aus einer Hauptfunktion {{Incode|main()}} und einer Weiche, an der überprüft wird, ob das Makro als Behälter für Klassen, Methoden usw. verwendet wird oder ob es eigenständig läuft. Nur die zweite Möglichkeit startet die Hauptfunktion {{Incode|main()}}. Noch ist diese Funktion leer.


<span id="Find_driving_constraints"></span>
===Find driving constraints===
===Randbedingungen zum Antreiben finden===


Die Randbedingungen zum Antreiben (Antriebe) sind Objekte in einem FreeCAD-Dokument. Sie müssen markiert sein, um gefunden zu werden.
The driving constraints are objects within a FreeCAD document. They need to be marked so that they can be found.


For this controller the suffix {{Incode|"Driver"}} has to be attached to the label of a driving constraint. It may be separated by a {{Incode|"."}} or {{Incode|"-"}} for clarity, as we will only check if the label ends with {{Incode|"Driver"}}.
Für diese Steuerung muss das Suffix {{Incode|"Driver"}} an das Label eines Antriebs angehängt werden. Es kann zur Verdeutlichung mit einem {{Incode|"."}} oder {{Incode|"-"}} abgetrennt werden, da nur geprüft wird ob das Label auf {{Incode|"Driver"}} endet.


Eine Funktion, der das Dokumentobjekt übergeben wird und die eine Liste von Antrieben zurück gibt, wird diese Arbeit erledigen.
A function that receives a document object and returns a list of driving constraints (the names in this case) will do the job.


{{Code|code=
{{Code|code=
Line 56: Line 60:
}}
}}


The {{Incode|main()}} function loads the active document into the variable {{Incode|kin_doc}} and then calls the function {{Incode|findTheDrivingConstraints()}} and hands over the content of {{Incode|kin_doc}}. The returned list is loaded into {{Incode|drivers}} which is then checked to contain at least one item. If that is the case the list is finally printed to the [[Report_view|Report view]].
Die Hauptfunktion {{Incode|main()}} lädt das aktive Dokument in die Variable {{Incode|kin_doc}} und ruft im Anschluss die Funktion {{Incode|findTheDrivingConstraints()}} (Antriebs-Randbedingungen finden) auf und übergibt den Inhalt von {{Incode|kin_doc}}. Die zurückgegebene Liste wird in die Variable {{Incode|drivers}} abgelegt, die dann überprüft wird, ob sie wenigstens ein Element enthält. Wenn das der Fall ist, wird die Liste im [[Report_view|Ausgabefenster]] angezeigt.


{{Code|code=
{{Code|code=
Line 70: Line 74:
<div class="mw-collapsible mw-collapsed">
<div class="mw-collapsible mw-collapsed">


'''The macro so far...'''
'''Das Makro bis hierhin...'''


<div class="mw-collapsible-content">
<div class="mw-collapsible-content">
Line 105: Line 109:
</div> <!-- End of collapsible element for ... section. Do not remove! -->
</div> <!-- End of collapsible element for ... section. Do not remove! -->
{{Top}}
{{Top}}
<span id="Control_panel"></span>
===Control panel===
===Steuerungsfenster===


Das Steuerungsfenster ist aus Qt-Widgets ('''wi'''ndow ga'''dgets''' ~ Fenstergelöt) aufgebaut, einem Hauptfenster und einigen Ein- und Ausgabe-Widgets.
The control panel is built from Qt widgets, one main window containing several input/output widgets.


Alle Widgets müssen importiert werden, bevor sie verwendet werden können, aber sie können als ein kompletter Satz importiert werden.
Each widget has to be imported before it can be used, but they can be imported as a single set. The import line is placed near the top of the file.


<span id="Main_window"></span>
====Main window====
====Hauptfenster====


Für das Hauptfenster sieht die Import-Zeile so aus:
For the main window the import line looks like this:


{{Code|code=
{{Code|code=
Line 119: Line 125:
}}
}}


The main window called {{Incode|ControlPanel}} is a class object instantiated from the {{Incode|QDialog}} widget.
Das Hauptfenster, {{Incode|ControlPanel}} genannt, ist ein Klassenobjekt, das eine Instanz des {{Incode|QDialog}}-Widgets ist.


It has two init methods. {{Incode|__init__()}} initializes the new class object, handles incoming arguments, and starts {{Incode|initUI()}} which manages all widgets within the main window.
Es enthält zwei init-Methoden. {{Incode|__init__()}} initialisiert das neue Klassenobjekt, verarbeitet eingehende Variablen und startet {{Incode|initUI()}}, die alle Widgets innerhalb des Hauptfensters verwaltet.


{{Code|code=
{{Code|code=
Line 139: Line 145:
}}
}}


To launch a single control panel an instance, called {{Incode|panel}}, of this class will be created with {{Incode|kin_doc}} (the document object) and {{Incode|drivers[0]}} (the first in the list of driving constraints) transferred to this instance. Finally the {{Incode|exec_()}} method of the class opens the dialog window.
Um ein einzelnes Steuerungsfenster zu starten, wird eine Instanz dieser Klasse namens {{Incode|panel}} erstellt, mit {{Incode|kin_doc}} (dem Dokumentobjekt) und {{Incode|drivers[0]}} (dem ersten Element auf der Liste der Antriebe), die an diese Instanz übergeben wurden. Schließlich öffnet die Methode {{Incode|exec_()}} der Klasse das Dialogfenster.


{{Code|code=
{{Code|code=
Line 146: Line 152:
}}
}}


Um mehr als eine Antrieb zu verwenden, muss die Liste der Antriebe durchgesehen werden und für jedes Element der Liste eine Instanz erstellt werden, der das jeweilige Element übergeben wird.
To handle more than one driver, we have to check the drivers list and create an instance for each item in the list and transfer the current item.


{{Code|code=
{{Code|code=
Line 156: Line 162:
}}
}}


These lines replace the {{Incode|print()}} command in the else branch of the {{Incode|main()}} function.
Diese Zeilen ersetzen den Ausgabebefehl {{Incode|print()}} im else-Abschnitt der Hauptfunktion {{Incode|main()}}.


Note: Collecting a {{Incode|panel_list}} allows us to launch all panels at once. (I cannot explain this behaviour yet...)
Hinweis: Das Zusammenstellen einer {{Incode|panel_list}} (Fensterliste) erlaubt es alle Fenster auf einmal zu starten. (Ich kann dieses Verhalten bisher nicht erklären...)


Lässt man das Makro laufen, erstellt es ein sauberes, leeres Dialogfenster, das auf Widgets wartet.
Running the macro will display a clean empty dialog window waiting for widgets:


[[Image:Tutorial_KinCon-01.png|300px|An empty dialog window]]
[[Image:Tutorial_KinCon-01.png|300px|Ein leeres Dialogfenster]]


<div class="mw-collapsible mw-collapsed">
<div class="mw-collapsible mw-collapsed">


'''And the macro so far...'''
'''Das Makro bis hierhin...'''


<div class="mw-collapsible-content">
<div class="mw-collapsible-content">
Line 223: Line 229:
</div> <!-- End of collapsible element for ... section. Do not remove! -->
</div> <!-- End of collapsible element for ... section. Do not remove! -->
{{Top}}
{{Top}}
<span id="Setting_parameters"></span>
====Setting parameters====
====Parameter setzen====


Now it is time to fill the {{Incode|initUI()}} method:
Nun ist es Zeit die Methode {{Incode|initUI()}} zu befüllen:


{{Code|code=
{{Code|code=
...
...
def initUI(self, Document, actuator_list):
def initUI(self, document, actuator):
# Setting up class parameters
# Setting up class parameters
self.document = Document
self.actuator = document.getObject(actuator)
self.actuators = actuator_list
self.actuator = self.document.getObject(self.actuators[0])
self.driver_type = self.getDriverType(self.actuator)
self.driver_type = self.getDriverType(self.actuator)
# the window has 640 x 480 pixels and is centered by default
# the window has 640 x 480 pixels and is centered by default
Line 241: Line 246:
}}
}}


{{Incode|self.actuator}} repräsentiert den Antrieb und {{Incode|self.driver_type}} Speichert ein Schlüsselwort für die Art des Antriebs. Letztere sorgt dafür, dass jeder Randbedingung die richtige Eigenschaft zugeordnet wird.
The actuator to be used is the first item in the actuators list. (The list contains a single item now, but, if the controller can handle more than one driving constraint in the future, it may hold more.)


<span id="Method_getDriverType()"></span>
=====Method getDriverType()=====
=====Methode getDriverType()=====


For later use we need the driver type (Angle, Distance, Length) and so a {{Incode|getDriverType()}} method has to be defined:
Für spätere Verwendung muss die Art des Antriebs ermittelt werden, Winkel, Abstand oder Länge (Angle, Distance, Length), daher muss eine Methode namens {{Incode|getDriverType()}} (Finde den Antriebstyp) definiert werden.


{{Code|code=
{{Code|code=
Line 251: Line 257:
def getDriverType(self, constraint):
def getDriverType(self, constraint):
ANGLE_CONSTRAINTS = [
ANGLE_CONSTRAINTS = [
"Angle",
"PlaneCoincident",
"PlaneCoincident",
"AxialAlignment",
"AxialAlignment",
"PlaneAlignment"
"PlaneAlignment"
]
] # Angel constraints that use negative values, too
ANGLE_DEG_CONSTRAINTS = [
"Angle",
] # Angel constraints that use positive values only
DISTANCE_CONSTRAINTS = [
DISTANCE_CONSTRAINTS = [
"PointDistance",
"PointDistance",
"PointsDistance"
"PointsDistance"
]
] # Lenght constraints that use positive values only
if constraint.ConstraintType in ANGLE_CONSTRAINTS:
if constraint.ConstraintType in ANGLE_CONSTRAINTS:
return "Angle"
return "Angle"
elif constraint.ConstraintType in ANGLE_DEG_CONSTRAINTS:
return "AnglePos"
elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
return "Distance"
return "Distance"
Line 269: Line 279:
}}
}}


Diese Methode überprüft, ob die Art der gegebenen Randbedingung in einer der Listen zu finden ist, und gibt zurück, welche Art Maß zu steuern ist
This method checks if the type of the given constraint can be found in one of the lists, and returns which kind of dimension has to be controlled.


It is assumed that in the kinematic document the driver is marked correctly and working if edited manually. In this case there is no need to filter out geometric constraints such as Colinear or PointsCoincidence (but here would be the place to do so...)
Es wird angenommen, dass der Antrieb im Kinematikdokument korrekt markiert wurde und funktioniert, wenn er von Hand geändert wird. In diesem Falle ist es nicht nötig geometrische Randbedingungen wie Colinear oder PointsCoincidence herauszufiltern (aber hier wäre die Stelle es zu tun...)


<span id="Window_properties"></span>
=====Window properties=====
=====Fenstereigenschaften=====


Die Fenstergröße wird über seine Größt- und Kleinstmaße festgelegt. Verwendet man jeweils dieselben Werte, wird eine konstante Größe festgelegt
The window size is defined by its minimum and maximum dimensions. Using the same values results in a fixed size.


Im Titel findet sich der Name des Antriebs und ob es sich um einen Winkel, einen Abstand oder eine Länge handelt. Schließlich wird das Fenster angewiesen über allen anderen Fenstern (sichtbar) zu bleiben.
The title shows the driver name and whether its an angle, a distance, or a length. Finally the window is told to stay on top of all windows.


{{Code|code=
{{Code|code=
Line 293: Line 304:
}}
}}
{{Top}}
{{Top}}
<span id="Setting_more_parameters"></span>
====Setting more parameters====
====Weitere Parameter setzen====


Im nächsten Schritt wird der Wert des Antriebs ausgelesen und die Start- und Endwerte abhängig von der Antriebsart gesetzt.
The next step is to extract the current value of the driver and set the default start and end values depending on the driver type.


Ein Abstand kann nicht negativ sein und exakt null verwirrt den Gleichungslöser, daher wird der Startwert auf 0,001 gesetzt. Winkel akzeptieren negative Werte und erhalten symmetrische Werte. (Ob Längen negative Werte akzeptieren, muss noch abschließend festgestellt werden...)
A distance cannot be negative and exactly zero puzzles the solver and so the start value is set to 0.001. Angles accept negative values and get symmetric values. (If lengths accept negative values has to be proven finally...)


Die Einheiten müssen gespeichert werden, bis die Werte am Ende an die Eigenschaften der Randbedingungen zurückgegeben werden. Abstände und Längen benötigen Werte mit Einheiten.
The unit suffix must be kept for returning the value to the constraint property in the end. Distances and lengths need values with units.


Der Umgang mit Einheiten und die Darstellung der Werte als Zeichenketten in einigen Widgets erfordert es häufig Zahlen in Zeichenketten und wieder zurück zu verwandeln.
Dealing with units and displaying values as strings in several widgets requires to convert numbers into strings and back again quite often.


To complete the parameters we set a default number of steps that should be computed when the motion is automated and if the {{Incode|self.sequence}} toggle is set to {{Incode|True}}, a picture will be taken with each step of the motion.
Um die Parameter zu vervollständigen, wird ein Wert für die Anzahl von Schritten vorgegeben, die berechnet werden solle, wenn die Bewegung automatisch erfolgt; und wenn der Umschalter {{Incode|self.sequence}} auf {{Incode|True}} gesetzt ist, wird bei jedem Schritt der Bewegung ein Bild aufgenommen.


{{Code|code=
{{Code|code=
Line 313: Line 325:
self.start_value = (self.current_value - 15)
self.start_value = (self.current_value - 15)
self.end_value = (self.current_value + 15)
self.end_value = (self.current_value + 15)
self.unit_suffix = (" °")
elif self.driver_type == "AnglePos":
self.current_value = float(str(self.actuator.Angle)[:-4])
self.start_value = 0.001 # Angle must not be <= 0 and >=180
self.end_value = 179.999
self.unit_suffix = (" °")
self.unit_suffix = (" °")
elif self.driver_type == "Distance":
elif self.driver_type == "Distance":
Line 327: Line 344:
}}
}}
{{Top}}
{{Top}}
====Labels====
<span id="Labels"></span>
====Label====


Nun werden drei Label(-Widgets) hizugefügt, um Start-, End-, sowie den aktuellen Wert anzuzeigen
Now three labels are added to display the start, end, and current value.


First the class {{Incode|QLabel}} must be imported i.e. the import list has to be extended like this:
Zuerst muss die Klasse {{Incode|QLabel}} importiert werden, d. h. die Import-Liste muss, wie hier gezeigt wird, ergänzt werden:


{{Code|code=
{{Code|code=
Line 337: Line 355:
}}
}}


Back in the {{Incode|initUI()}} method we insert:
Zurück in der Methode {{Incode|initUI()}} fügt man folgendes ein:


{{Code|code=
{{Code|code=
Line 359: Line 377:
}}
}}


The placement is done with the inherited {{Incode|setGeometry()}} method. In this case the description of a rectangle is used (X position, Y position, width, height).
Die Platzierung erfolgt mit der ererbten Methode {{Incode|setGeometry()}}. In diesem Falle wird die Beschreibung eines Rechtecks verwendet (X-Position, Y-Position, Breite, Höhe)


Die ersten und dritten Zeilen könnten zusammengefasst werden, wird aber aus Gründen der Übersichtlichkeit nicht empfohlen.
The first and third lines could be combined, but it is not recommended for clarity reasons:


{{Code|code=
{{Code|code=
Line 367: Line 385:
}}
}}


Startet man das Makro aus einem Kinematikdokument heraus, erhält man ein Dialogfenster wie dieses:
Running the macro with a kinematic assembly document would create a dialog window like this:


[[Image:Tutorial_KinCon-02.png|300px|A dialog window displaying start value, current Value, and end value]]
[[Image:Tutorial_KinCon-02.png|300px|Ein Dialogfenster, das den Startwert, den aktuellen Wert und den Endwert anzeigt]]
{{Caption|Das Dialogfenster zeigt in der Titelleiste das Label der Randbedingung und die Art des Antriebs an, sowie den Startwert, den aktuellen Wert und den Endwert in der ersten Zeile des Hauptbereiches}}
{{Caption|The dialog window displaying the constraint label and driver type in the title, and the start value, current value and end value on the first line in the main area}}


<div class="mw-collapsible mw-collapsed">
<div class="mw-collapsible mw-collapsed">


'''And the macro so far...'''
'''Das Makro bis hierhin...'''


<div class="mw-collapsible-content">
<div class="mw-collapsible-content">
Line 404: Line 422:
self.start_value = (self.current_value - 15)
self.start_value = (self.current_value - 15)
self.end_value = (self.current_value + 15)
self.end_value = (self.current_value + 15)
self.unit_suffix = (" °")
elif self.driver_type == "AnglePos":
self.current_value = float(str(self.actuator.Angle)[:-4])
self.start_value = 0.001 # Angle must not be <= 0 and >=180
self.end_value = 179.999
self.unit_suffix = (" °")
self.unit_suffix = (" °")
elif self.driver_type == "Distance":
elif self.driver_type == "Distance":
Line 446: Line 469:
def getDriverType(self, constraint):
def getDriverType(self, constraint):
ANGLE_CONSTRAINTS = [
ANGLE_CONSTRAINTS = [
"Angle",
"PlaneCoincident",
"PlaneCoincident",
"AxialAlignment",
"AxialAlignment",
"PlaneAlignment"
"PlaneAlignment"
]
] # Angel constraints that use negative values, too
ANGLE_DEG_CONSTRAINTS = [
"Angle",
] # Angel constraints that use positive values only
DISTANCE_CONSTRAINTS = [
DISTANCE_CONSTRAINTS = [
"PointDistance",
"PointDistance",
Line 457: Line 482:
if constraint.ConstraintType in ANGLE_CONSTRAINTS:
if constraint.ConstraintType in ANGLE_CONSTRAINTS:
return "Angle"
return "Angle"
elif constraint.ConstraintType in ANGLE_DEG_CONSTRAINTS:
return "AnglePos"
elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
return "Distance"
return "Distance"
Line 495: Line 522:
</div> <!-- End of collapsible element for ... section. Do not remove! -->
</div> <!-- End of collapsible element for ... section. Do not remove! -->
{{Top}}
{{Top}}
====Slider====
<span id="Slider"></span>
====Schieberegler====


Um den aktuellen Wert auf jede mögliche Zahl zwischen Start- und Endwert einzustellen, würde ein Schieberegler passen.
To change the current value to any number between start and end value a slider widget would fit.


First the class {{Incode|QSlider}} must be imported i.e. the import list has to be extended like this:
Zuerst muss die Klasse {{Incode|QSlider}} importiert werden, d. h. die Import-Liste muss, wie hier gezeigt wird, ergänzt werden:


{{Code|code=
{{Code|code=
Line 505: Line 533:
}}
}}


Back in the {{Incode|initUI()}} method and right after the labels section we insert:
Zurück in der Methode {{Incode|initUI()}} fügt man folgendes direkt nach dem Labels-Abschnitt ein:


{{Code|code=
{{Code|code=
Line 521: Line 549:
}}
}}


The slider button is placed with the {{Incode|setValue()}} method. Its value has to be calculated from the current value and a step ratio. The ratio has to be calculated whenever a start or end value is changed and so we insert another method after the {{Incode|getDriverType()}} method.
Der Schieberknopf wird mit der Methode {{Incode|setValue()}} platziert. Sein Wert muss aus dem aktuellen Wert und dem Schrittverhältnis berechnet werden. Das Schrittverhältnis muss immer berechnet werden, wenn sich der Start- oder Endwert ändert, daher wird eine weitere Methode hinter der Methode {{Incode|getDriverType()}} eingefügt.


Ein Verhältnis zu verwenden anstatt die Kleinst- und Größtwerte des Schiebereglers zu verändern, hat den Vorteil einer feineren Auflösung für kleinere Werte.
To work with a ratio instead of altering the slider's min and max values has the advantage of a finer resolution for small values.


{{Code|code=
{{Code|code=
Line 533: Line 561:
}}
}}


And after this one comes another method defining what to do when the slider position or the slider value changes. The {{Incode|onActuatorSlider()}} method is called by the {{Incode|connect()}} method which also hands over the slider value as an argument.
Und nach diesem kommt eine weitere Methode, die festlegt, was zu tun ist, wenn sich die Position des Schiebers bzw. der Wert des Schiebereglers ändert. Die Methode {{Incode|onActuatorSlider()}} wird von der Methode {{Incode|connect()}} aufgerufen, die auch den Wert des Schhiebereglers als Variable übergibt.


It recalculates the current value from the slider position, rewrites the text of the label {{Incode|self.label_current}} and changes the constraint property according to the driver type.
Sie berechnet den aktuellen Wert erneut aus der Schieberposition, überschreibt den Text des Labels {{Incode|self.label_current}} und ändert die Eigenschaft der Randbedingung unter Beachtung der Art des Antriebs.


Running the command {{Incode|"asm3CmdQuickSolve"}} starts the solver to rearrange the assembly parts with the altered value.
Wird der Befehl {{Incode|"asm3CmdQuickSolve"}} gestartet, beginnt der Gleichnungslöser die Bestandteile der Baugruppe dem geänderten Wert entsprechend neu anzuordnen.


{{Code|code=
{{Code|code=
Line 544: Line 572:
self.current_value = slider_value * self.stepRatio() + self.start_value
self.current_value = slider_value * self.stepRatio() + self.start_value
if self.driver_type == "Angle":
if self.driver_type == "Angle":
self.actuator.Angle = self.current_value
elif self.driver_type == "AnglePos":
self.actuator.Angle = self.current_value
self.actuator.Angle = self.current_value
elif self.driver_type == "Distance":
elif self.driver_type == "Distance":
Line 554: Line 584:
}}
}}


Das Dialogfenster mit dem Schieberegler sollte so aussehen und ist jetzt bereit eine Bewegung zu steuern.
The dialog window with the slider should look like this and is ready to control a motion:


[[Image:Tutorial_KinCon-03.png|300px|Two dialog windows with a slider]]
[[Image:Tutorial_KinCon-03.png|300px|Zwei Dialogfenster mit Schieberegler]]
{{Caption|Dialogfenster mit dem hinzugefügten Schieberegler, eins für einen Winkelantrieb (Drehantrieb) und eins für einen (Abstands-/) Linearantrieb}}
{{Caption|Dialog windows with the added slider, one for an Angle driver and one for a Distance driver}}


Es kann ein Dialogfenster für jedes geöffnete Dokument gestartet werden; sie beeinflussen sich nicht gegenseitig.
We can start a dialog window for any opened document, they won't interfere with each other.
{{Top}}
{{Top}}
<span id="Text_entry_fields"></span>
====Text entry fields====
====Texteingabefelder====


Um Start- und Endwerte zu ändern kann man ein Line-Edit-Widget verwenden.
To set the start and end value we use a line edit widget.


First the class {{Incode|QLineEdit}} must be imported i.e. the import list has to be extended like this:
Zuerst muss die Klasse {{Incode|QLineEdit}} importiert werden, d. h. die Import-Liste muss, wie hier gezeigt wird, ergänzt werden:


{{Code|code=
{{Code|code=
Line 571: Line 602:
}}
}}


Back in the {{Incode|initUI()}} method and between the labels and the slider sections we insert:
Zurück in der Methode {{Incode|initUI()}} fügt man folgendes zwischen Labels- und Slider-Abschnitt ein:


{{Code|code=
{{Code|code=
Line 589: Line 620:
}}
}}


The entry fields display the default start and end values. They are not complete until we add the methods to deal with altered entries. This will be done by the methods {{Incode|self.onEntryStart()}} and {{Incode|self.onEntry()}} that are inserted between the {{Incode|self.stepRatio()}} and the {{Incode|self.onActuatorSlider()}} methods.
Die Eingabefelder zeigen die voreingestellten Start- und Endwerte an. Sie sind aber erst komplett, wenn auch Methoden hinzugefügt werden, die die geänderten Werte verarbeiten. Das erledigen die Methoden {{Incode|self.onEntryStart()}} und {{Incode|self.onEntryEnd()}}, die zwischen den Methoden {{Incode|self.stepRatio()}} und {{Incode|self.onActuatorSlider()}} eingefügt werden.


{{Code|code=
{{Code|code=
Line 609: Line 640:
}}
}}


Both convert the received string value to a floating point number and change either {{Incode|self.start_value}} or {{Incode|self.end_value}} and the corresponding label accordingly. After that the slider value is updated.
Beide wandeln die erhaltene Zeichenkette in eine Fließkommazahl und ändern den Startwert {{Incode|self.start_value}} bzw. Endwert {{Incode|self.end_value}} und das dazugehörige Label. Danach wird der Schieber aktualisiert.


Das Dialogfenster mit den Texteingabefeldern sollte so aussehen und ist jetzt bereit den Bereich einer Bewegung zu ändern:
The dialog window with text entry fields should look like this and is ready to change the range of a motion:


[[Image:Tutorial_KinCon-04.png|300px|Two dialog windows with line edit fields]]
[[Image:Tutorial_KinCon-04.png|300px|Zwei Dialogfenster mit Texteingabefeldern]]
{{Caption|Dialogfenster mit Texteingabefeldern, nochmals für Drehantrieb und Linearantrieb}}
{{Caption|Dialog windows with line edit fields, again for an angle and a distance driver}}


<div class="mw-collapsible mw-collapsed">
<div class="mw-collapsible mw-collapsed">


'''And the macro so far...'''
'''Das Makro bis hierhin...'''


<div class="mw-collapsible-content">
<div class="mw-collapsible-content">
Line 648: Line 679:
self.start_value = (self.current_value - 15)
self.start_value = (self.current_value - 15)
self.end_value = (self.current_value + 15)
self.end_value = (self.current_value + 15)
self.unit_suffix = (" °")
elif self.driver_type == "AnglePos":
self.current_value = float(str(self.actuator.Angle)[:-4])
self.start_value = 0.001 # Angle must not be <= 0 and >=180
self.end_value = 179.999
self.unit_suffix = (" °")
self.unit_suffix = (" °")
elif self.driver_type == "Distance":
elif self.driver_type == "Distance":
Line 714: Line 750:
def getDriverType(self, constraint):
def getDriverType(self, constraint):
ANGLE_CONSTRAINTS = [
ANGLE_CONSTRAINTS = [
"Angle",
"PlaneCoincident",
"PlaneCoincident",
"AxialAlignment",
"AxialAlignment",
"PlaneAlignment"
"PlaneAlignment"
]
] # Angel constraints that use negative values, too
ANGLE_DEG_CONSTRAINTS = [
"Angle",
] # Angel constraints that use positive values only
DISTANCE_CONSTRAINTS = [
DISTANCE_CONSTRAINTS = [
"PointDistance",
"PointDistance",
"PointsDistance"
"PointsDistance"
]
] # Lenght constraints that use positive values only
if constraint.ConstraintType in ANGLE_CONSTRAINTS:
if constraint.ConstraintType in ANGLE_CONSTRAINTS:
return "Angle"
return "Angle"
elif constraint.ConstraintType in ANGLE_DEG_CONSTRAINTS:
return "AnglePos"
elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
return "Distance"
return "Distance"
Line 751: Line 791:
self.current_value = slider_value * self.stepRatio() + self.start_value
self.current_value = slider_value * self.stepRatio() + self.start_value
if self.driver_type == "Angle":
if self.driver_type == "Angle":
self.actuator.Angle = self.current_value
elif self.driver_type == "AnglePos":
self.actuator.Angle = self.current_value
self.actuator.Angle = self.current_value
elif self.driver_type == "Distance":
elif self.driver_type == "Distance":
Line 793: Line 835:
</div> <!-- End of collapsible element for ... section. Do not remove! -->
</div> <!-- End of collapsible element for ... section. Do not remove! -->
{{Top}}
{{Top}}
===Motion===
<span id="Motion"></span>
===Bewegung===


um einen Zusammenbau in Bewegung zu setzen benötigt man:
To get the assembly in motion we need:
* Schaltflächen, die eine Bewegung in die gewünschte Richtung auslösen.
* Buttons to trigger motion in the desired direction.
* Ein Eingabefeld, um die Anzahl der schritte zu verändern, für schnellere oder fließendere Bewegungen.
* An input field to alter the number of steps for faster or smoother motions.
* Ein Optionsfeld (Checkbox), um das Erstellen einer Bildfolge zu aktivieren.
* A check box to indicate if we want to shoot a sequence of images.


<span id="Forward_and_Backward_buttons"></span>
====Forward and Backward buttons====
====Vorwärts- und Rückwärts-Schaltflächen====


Damit sich die Bauteile automatisch bewegen, werden zwei Schaltflächen benötigt, die die Bewegungen auslösen, eine in Richtung der Startposition und eine in Richtung der Endposition. Diese beiden und eine zum Schließen verwenden das Widget {{Incode|QPushButton}}.
To move the assembly parts automatically we need two buttons to trigger the motions, one towards the start position and one towards the end position. These two and a close button will use a {{Incode|QPushButton}} widget.


Small assemblies compute a bit too fast and show jumps instead of a smooth motion. To slow it down we use the {{Incode|sleep()}} method of the {{Incode|time}} module which has to be imported first.
Kleinere Baugruppen werde etwas zu schnell berechnet und zeigen eher Sprünge anstatt einer fließenden Bewegung. Um sie zu verlangsamen wird die Methode {{Incode|sleep()}} des Moduls {{Incode|time}} verwendet, das zuerst importiert werden muss.


Ein weiterer Import und ein weiteres Widget:
Another import and another widget:


{{Code|code=
{{Code|code=
Line 813: Line 857:
}}
}}


Back in the {{Incode|initUI()}} method we insert the buttons after the slider section:
Zurück in der Methode {{Incode|initUI()}} fügt man folgendes nach dem Slider-Abschnitt ein:


{{Code|code=
{{Code|code=
Line 835: Line 879:
}}
}}


The methods dealing with pressed buttons are {{Incode|self.onForward()}}, {{Incode|self.onBackward()}}, and {{Incode|self.onClose()}}. They are inserted after the {{Incode|onActuatorSlider()}} method.
Die Methoden, die gedrückte Schaltflächen bearbeiten sind {{Incode|self.onForward()}}, {{Incode|self.onBackward()}}, und {{Incode|self.onClose()}}. Sie werden nach der Methode {{Incode|onActuatorSlider()}} eingefügt.


{{Code|code=
{{Code|code=
Line 868: Line 912:
}}
}}


The method {{Incode|self.onClose()}} invokes the inherited method {{Incode|self.close()}} which just closes the dialog window and thereby ends the macro.
Die Methode {{Incode|self.onClose()}} ruft die ererbte Methode {{Incode|self.close()}} auf, die nur das Dialogfenster schließt und damit das Makro beendet.


Both {{Incode|self.onForward()}} and {{Incode|self.onBackward()}} count the steps that are left to go to reach the wanted position and calculate the length of a step according to the number of steps. For now we go with the default number of 10 steps.
Beide Methoden {{Incode|self.onForward()}} und {{Incode|self.onBackward()}} zählen die verbleibenden Schritte, die noch zu gehen sind, bis man die gewünschte Position erreicht und berechnen die Schrittlänge entsprechend der Anzahl der Schritte. Zu diesem Zeitpunkt wird die voreingestellte Anzahl von zehn Schritten beibehalten.


Jede Runde in der While-Schleife erhöht/verringert den aktuellen Wert und aktualisiert den Schieber, wodurch im Hintergrund {{Incode|onActuatorSlider()}} ausgelöst wird (siehe Absatz [[#Schieberegler|Schieberegler]]).
Each round on the while loop increases/decreases the current value and updates the slider values which triggers {{Incode|onActuatorSlider()}} in the background (see [[#Slider|Slider paragraph]]). After a pause to let the computer provide another updated 3D view, counting down the steps left to go finishes the loop.


Wenn keine Schritte mehr übrig sind, wird der Schieber auf die erste bzw. letzte Position gesetzt, nur für den Fall, dass Rundungsfehler aufgetreten sind.
With no steps left the slider is set to the first/last slider position, just in case if a rounding error had occurred.


Das Dialogfenster mit den Schalflächen sollte so aussehen und kann jetzt einen Zusammenbau mit 10 Schritten auf die gewünschte Position bewegen:
The dialog window with buttons should look like this and can now move the assembly by 10 steps towards the wanted start/end position:


[[Image:Tutorial_KinCon-05.png|300px|Dialog window with buttons]]
[[Image:Tutorial_KinCon-05.png|300px|Dialogfenster mit Schaltflächen]]
{{Caption|Dialog window with buttons}}
{{Caption|Dialogfenster mit Schaltflächen}}


<div class="mw-collapsible mw-collapsed">
<div class="mw-collapsible mw-collapsed">


'''And the macro so far...'''
'''Das Makro bis hierhin...'''


<div class="mw-collapsible-content">
<div class="mw-collapsible-content">
Line 914: Line 958:
self.start_value = (self.current_value - 15)
self.start_value = (self.current_value - 15)
self.end_value = (self.current_value + 15)
self.end_value = (self.current_value + 15)
self.unit_suffix = (" °")
elif self.driver_type == "AnglePos":
self.current_value = float(str(self.actuator.Angle)[:-4])
self.start_value = 0.001 # Angle must not be <= 0 and >=180
self.end_value = 179.999
self.unit_suffix = (" °")
self.unit_suffix = (" °")
elif self.driver_type == "Distance":
elif self.driver_type == "Distance":
Line 996: Line 1,045:
def getDriverType(self, constraint):
def getDriverType(self, constraint):
ANGLE_CONSTRAINTS = [
ANGLE_CONSTRAINTS = [
"Angle",
"PlaneCoincident",
"PlaneCoincident",
"AxialAlignment",
"AxialAlignment",
"PlaneAlignment"
"PlaneAlignment"
]
] # Angel constraints that use negative values, too
ANGLE_DEG_CONSTRAINTS = [
"Angle",
] # Angel constraints that use positive values only
DISTANCE_CONSTRAINTS = [
DISTANCE_CONSTRAINTS = [
"PointDistance",
"PointDistance",
"PointsDistance"
"PointsDistance"
]
] # Lenght constraints that use positive values only
if constraint.ConstraintType in ANGLE_CONSTRAINTS:
if constraint.ConstraintType in ANGLE_CONSTRAINTS:
return "Angle"
return "Angle"
elif constraint.ConstraintType in ANGLE_DEG_CONSTRAINTS:
return "AnglePos"
elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
return "Distance"
return "Distance"
Line 1,033: Line 1,086:
self.current_value = slider_value * self.stepRatio() + self.start_value
self.current_value = slider_value * self.stepRatio() + self.start_value
if self.driver_type == "Angle":
if self.driver_type == "Angle":
self.actuator.Angle = self.current_value
elif self.driver_type == "AnglePos":
self.actuator.Angle = self.current_value
self.actuator.Angle = self.current_value
elif self.driver_type == "Distance":
elif self.driver_type == "Distance":
Line 1,102: Line 1,157:
</div> <!-- End of collapsible element for ... section. Do not remove! -->
</div> <!-- End of collapsible element for ... section. Do not remove! -->
{{Top}}
{{Top}}
<span id="Number_of_steps"></span>
==== Number of steps ====
==== Anzahl der Schritte ====


Die Voreinstellung ist so gewählt, dass man einen schnellen Eindruck erhält, ob sich der Zusammenbau wie erwartet bewegt, ohne zu viel Rechenzeit zu verbrauchen.
The default setting is to get a quick impression if the assembly is moving as expected without wasting too much computing time.


Wenn die Bauteile eher springen als sich fließend zu bewegen, oder wenn winkelbasierte Antriebe Problemen bereiten, wenn der Unterschied zwischen zwei Winkeln zu groß ist, dann kann beides durch das Erhöhen der Anzahl der Schritte behoben werden.
If the parts jump rather than move smoothly, or if drivers based on angles tend to cause trouble when the difference between two angles is too large, then both can be fixed by increasing the number of steps.


Und ein weiteres Line-Edit-Widget wird verwendet, um die Anzahl der Schritte zu ändern (eingesetzt hinter den schon vorhandenen Line-Edit-Widgets):
And so another line edit widget is used to alter the number steps (placed after the existing line edit widgets):


{{Code|code=
{{Code|code=
Line 1,120: Line 1,176:
}}
}}


The related method {{Incode|self.onEntrySteps()}} just fills the parameter {{Incode|self.step_value}} with the entered value. It is inserted after the {{Incode|onEntryEnd()}} method.
Die zugehörige Methode {{Incode|self.onEntrySteps()}} füllt nur den Parameter {{Incode|self.step_value}} mit dem eingegebenen Wert. Sie wird nach der Methode {{Incode|onEntryEnd()}} eingefügt.


{{Code|code=
{{Code|code=
Line 1,129: Line 1,185:
}}
}}


Das Dialogfenster, das in der Lage ist, die Anzahl der Schritte zu ändern sollte so aussehen:
The dialog window able to change the number of steps should look like this:


[[Image:Tutorial_KinCon-06.png|300px|Dialog window with another text entry field]]
[[Image:Tutorial_KinCon-06.png|300px|Dialogfenster mit einem weiteren Texteingabefeld]]
{{Caption|Dialog window with another text entry field}}
{{Caption|Dialogfenster mit einem weiteren Texteingabefeld}}
{{Top}}
{{Top}}
<span id="Image_sequence"></span>
====Image sequence====
====Bildfolge====


Sobald die Bewegung der Bauteile den Erwartungen entspricht, kann bei jedem Schritt ein Bild aufgenommen werden. Aus der entstandenen Bildfolge kann eine kurze GIF-Animation erstellt werden.
When the motion of our assembly meets our expectations, we can take a picture of each step. The resulting sequence of images can be used to create a short gif animation.


To implement this functionality we need a {{Incode|QCheckBox}} widget, and a directory to store the images.
Um diese Funktionalität einzubauen, wird das Widget {{Incode|QCheckBox}} und ein Verzeichnis zum Speichern der Bilder gebraucht.


Ein weiterer Import und ein weiteres Widget:
One more import and widget:


{{Code|code=
{{Code|code=
Line 1,147: Line 1,204:
}}
}}


Back in the {{Incode|initUI()}} method we insert the check box after the slider section:
Zurück in der Methode {{Incode|initUI()}} fügt man folgendes nach dem Slider-Abschnitt ein:


{{Code|code=
{{Code|code=
Line 1,161: Line 1,218:
}}
}}


The method {{Incode|onOutputClicked()}} synchronises the parameter {{Incode|self.sequence}} and the display of the check mark.
Die Methode {{Incode|onOutputClicked()}} synchronisiert den Parameter {{Incode|self.sequence}} und die Anzeige des Aktivierungshäkchens.


{{Code|code=
{{Code|code=
Line 1,175: Line 1,232:
}}
}}


To define the output parameters we use the method {{Incode|output()}}:
Um die Ausgabeparameter festzulegen wird die Methode {{Incode|output()}} verwendet:


{{Code|code=
{{Code|code=
Line 1,200: Line 1,257:
}}
}}


Zuerst muss der Pfad zu den Bilddateien an das eigene Betriebssystem angepasst werden; der letzte Teil des Bildnamens bleibt ohne Zählnummer und Dateianhang. Dies muss zur Zeit von Hand gemacht werden.
First the image path has to be adapted to your OS; the last part is the image name without current number and file tag. This must be done manually for now.


Then follow file tag to finish the image name, image height and width, and how the background should be filled ({{Incode|"Current"}} (3D view background), {{Incode|"White"}}, {{Incode|"Black"}}, or {{Incode|"Transparent"}}).
Dann folgt der Dateianhang, um den Bildnamen abzuschließen, Bildhöhe und -Breite sowie eine Angabe, wie der Bildhintergrund gefüllt werden soll ({{Incode|"Current"}} (aktueller Hintergrund der 3D-Ansicht), {{Incode|"White"}} (weiß), {{Incode|"Black"}} (schwarz), oder {{Incode|"Transparent"}}).


Damit immer eine dreistellige Zahl verwendet wird, müssen führende Nullen vor der Zählvariable eingefügt werden.
To always have a 3 digit number leading zeros have to be prefixed to the counter parameter.


Finally the scripted version of the command [[Image:Std_ViewScreenShot.svg|24px]] [[Std_ViewScreenShot|Std ViewScreenShot]] is used to take a picture based on the mentioned parameters.
Am Ende wird die Skriptvariante des Befehls [[Image:Std_ViewScreenShot.svg|24px]] [[Std_ViewScreenShot/de|Std AnsichtAufnehmen]] verwendet, um mit Hilfe der genannten Parameter ein Bild aufzunehmen.


Still no pictures taken!?! No problem, as this method doesn't get called yet, and so we need to insert a call in the while loop of {{Incode|onForward()}} and {{Incode|onBackward()}}. Right after {{Incode|time.sleep(0.2)}} we insert this line:
Es werden noch keine Bilder aufgenommen!?! Kein Problem, da diese Methode noch nicht aufgerufen wird, und daher muss noch ein Aufruf in die While-Schleife von {{Incode|onForward()}} und {{Incode|onBackward()}} eingefügt werden. Direkt nach {{Incode|time.sleep(0.2)}} wird diese Zeile eingefügt:


{{Code|code=
{{Code|code=
Line 1,216: Line 1,273:
}}
}}


Jetzt sollte das Makro bereit sein, einen (kinematischen) Zusammenbau zu steuern und Bilder für ein animiertes GIF-Billd aufzunehmen.
Now the macro should be ready to control an assembly and to take pictures for an animated gif.


Die endgültige Version des Dialogfensters:
The final version of the dialog window:


[[Image:Tutorial_KinCon-07.png|300px|Dialog window finished]]
[[Image:Tutorial_KinCon-07.png|300px|Fertiges Dialogfenster]]
{{Caption|Dialog windows finished}}
{{Caption|Fertiges Dialogfenster}}


<div class="mw-collapsible mw-collapsed">
<div class="mw-collapsible mw-collapsed">


'''And finally the whole macro'''
'''Und schließlich das ganze Makro'''


<div class="mw-collapsible-content">
<div class="mw-collapsible-content">


'''Don't forget to set the path in the output() method!'''
'''Nicht vergessen den Pfad in der Ausgabemethode output() anzugeben!'''


{{Code|code=
{{Code|code=
Line 1,258: Line 1,315:
self.start_value = (self.current_value - 15)
self.start_value = (self.current_value - 15)
self.end_value = (self.current_value + 15)
self.end_value = (self.current_value + 15)
self.unit_suffix = (" °")
elif self.driver_type == "AnglePos":
self.current_value = float(str(self.actuator.Angle)[:-4])
self.start_value = 0.001 # Angle must not be <= 0 and >=180
self.end_value = 179.999
self.unit_suffix = (" °")
self.unit_suffix = (" °")
elif self.driver_type == "Distance":
elif self.driver_type == "Distance":
Line 1,354: Line 1,416:
def getDriverType(self, constraint):
def getDriverType(self, constraint):
ANGLE_CONSTRAINTS = [
ANGLE_CONSTRAINTS = [
"Angle",
"PlaneCoincident",
"PlaneCoincident",
"AxialAlignment",
"AxialAlignment",
"PlaneAlignment"
"PlaneAlignment"
]
] # Angel constraints that use negative values, too
ANGLE_DEG_CONSTRAINTS = [
"Angle",
] # Angel constraints that use positive values only
DISTANCE_CONSTRAINTS = [
DISTANCE_CONSTRAINTS = [
"PointDistance",
"PointDistance",
"PointsDistance"
"PointsDistance"
]
] # Lenght constraints that use positive values only
if constraint.ConstraintType in ANGLE_CONSTRAINTS:
if constraint.ConstraintType in ANGLE_CONSTRAINTS:
return "Angle"
return "Angle"
elif constraint.ConstraintType in ANGLE_DEG_CONSTRAINTS:
return "AnglePos"
elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
return "Distance"
return "Distance"
Line 1,394: Line 1,460:
self.current_value = slider_value * self.stepRatio() + self.start_value
self.current_value = slider_value * self.stepRatio() + self.start_value
if self.driver_type == "Angle":
if self.driver_type == "Angle":
self.actuator.Angle = self.current_value
elif self.driver_type == "AnglePos":
self.actuator.Angle = self.current_value
self.actuator.Angle = self.current_value
elif self.driver_type == "Distance":
elif self.driver_type == "Distance":
Line 1,492: Line 1,560:
</div> <!-- End of collapsible element for ... section. Do not remove! -->
</div> <!-- End of collapsible element for ... section. Do not remove! -->
{{Top}}
{{Top}}
<span id="Some_imperfections"></span>
== Some imperfections ==
== Ein paar Schwachstellen ==


* Die Numerierung der Bildfolge ist umgekehrt, da die Variable steps_left (verbleibende Schritte) heruntergezählt wird.
* The order of the image sequence is reversed as we use the variable steps_left which is counted down.
* The image directory and the image name are hard-coded.
* Das Bildverzeichnis und der Name des Bildes sind im Makro hinterlegt (hard-coded).
* Mehrfach geöffnete Steuerungsfenster sind nicht synchronisiert.
* Multiple Controllers are not synchronised.

Latest revision as of 07:40, 12 April 2024

Other languages:
Tutorial
Topic
Kinematisteuerung erstellt mit Python
Level
Pythongrundkentnisse sind hilfreich
Time to complete
1 Stunde
Authors
FBXL5
FreeCAD version
0.20 und später
Example files
None
See also
None

Einleitung

Diese Anleitung beschreibt, wie man eine einfache Kinematiksteuerung aus ein paar Zeilen Python-Kode erstellt, die mit Baugruppen des Arbeitsbereiches Assembly3 verwendet werden kann.

Jeder Texteditor kann zum Erstellen von Kode genutzt werden. Mein Favorit ist Atom, aber FreeCADs eingebauter Editor funktioniert auch bestens.

Die folgenden Kode-Beispiele können kopiert, in eine leere Textdatei eingefügt und dann unter einem selbstgewählten Namen als *.py oder **.FCMacro abgespeichert werden.

Makroabschnitte

Grundstruktur

#! python
# -*- coding: utf-8 -*-
# (c) 2022 Your name LGPL

def main():
    pass

if __name__ == "__main__":
    # This will be true only if the file is "executed"
    # but not if imported as a module
    main()

Die Grundstruktur besteht aus einer Hauptfunktion main() und einer Weiche, an der überprüft wird, ob das Makro als Behälter für Klassen, Methoden usw. verwendet wird oder ob es eigenständig läuft. Nur die zweite Möglichkeit startet die Hauptfunktion main(). Noch ist diese Funktion leer.

Randbedingungen zum Antreiben finden

Die Randbedingungen zum Antreiben (Antriebe) sind Objekte in einem FreeCAD-Dokument. Sie müssen markiert sein, um gefunden zu werden.

Für diese Steuerung muss das Suffix "Driver" an das Label eines Antriebs angehängt werden. Es kann zur Verdeutlichung mit einem "." oder "-" abgetrennt werden, da nur geprüft wird ob das Label auf "Driver" endet.

Eine Funktion, der das Dokumentobjekt übergeben wird und die eine Liste von Antrieben zurück gibt, wird diese Arbeit erledigen.

def findTheDrivingConstraints(document_object):
    # search through the Objects and find the driving constraint
    driver_list = []
    for each in document_object.Objects:
        if each.Label.endswith("Driver"):
            driving_constraint = each.Name
            driver_list.append(driving_constraint)
    return driver_list

Die Hauptfunktion main() lädt das aktive Dokument in die Variable kin_doc und ruft im Anschluss die Funktion findTheDrivingConstraints() (Antriebs-Randbedingungen finden) auf und übergibt den Inhalt von kin_doc. Die zurückgegebene Liste wird in die Variable drivers abgelegt, die dann überprüft wird, ob sie wenigstens ein Element enthält. Wenn das der Fall ist, wird die Liste im Ausgabefenster angezeigt.

def main():
    kin_doc = App.ActiveDocument # Kinematic Document
    drivers = findTheDrivingConstraints(kin_doc)
    if len(drivers) < 1:
        print("No driver found!")
    else:
        print(drivers)

Das Makro bis hierhin...

#! python
# -*- coding: utf-8 -*-
# (c) 2021 Your name LGPL

def findTheDrivingConstraints(document_object):
    # search through the Objects and find the driving constraint
    driver_list = []
    for each in document_object.Objects:
        if each.Label.endswith("Driver"):
            driving_constraint = each.Name
            driver_list.append(driving_constraint)
    return driver_list

def main():
    kin_doc = App.ActiveDocument # Kinematic Document
    drivers = findTheDrivingConstraints(kin_doc)
    if len(drivers) < 1:
        print("No driver found!")
    else:
        print(drivers)

if __name__ == "__main__":
    # This will be true only if the file is "executed"
    # but not if imported as a module
    main()

Anfang

Steuerungsfenster

Das Steuerungsfenster ist aus Qt-Widgets (window gadgets ~ Fenstergelöt) aufgebaut, einem Hauptfenster und einigen Ein- und Ausgabe-Widgets.

Alle Widgets müssen importiert werden, bevor sie verwendet werden können, aber sie können als ein kompletter Satz importiert werden.

Hauptfenster

Für das Hauptfenster sieht die Import-Zeile so aus:

from PySide2.QtWidgets import (QDialog)

Das Hauptfenster, ControlPanel genannt, ist ein Klassenobjekt, das eine Instanz des QDialog-Widgets ist.

Es enthält zwei init-Methoden. __init__() initialisiert das neue Klassenobjekt, verarbeitet eingehende Variablen und startet initUI(), die alle Widgets innerhalb des Hauptfensters verwaltet.

class ControlPanel(QDialog):
    """
    docstring for ControlPanel.
    """
    def __init__(self, document, actuator):
        super(ControlPanel, self).__init__()
        self.initUI(document, actuator)

    def initUI(self, document, actuator):
        # Setting up class parameters
        # the window has 640 x 480 pixels and is centered by default
        # now make the window visible
        self.show()

Um ein einzelnes Steuerungsfenster zu starten, wird eine Instanz dieser Klasse namens panel erstellt, mit kin_doc (dem Dokumentobjekt) und drivers[0] (dem ersten Element auf der Liste der Antriebe), die an diese Instanz übergeben wurden. Schließlich öffnet die Methode exec_() der Klasse das Dialogfenster.

panel = ControlPanel(kin_doc, drivers[0])
panel.exec_()

Um mehr als eine Antrieb zu verwenden, muss die Liste der Antriebe durchgesehen werden und für jedes Element der Liste eine Instanz erstellt werden, der das jeweilige Element übergeben wird.

panel_list = []
for each_driver in drivers:
    panel = ControlPanel(kin_doc, each_driver)
    panel_list.append(panel)
panel.exec_()

Diese Zeilen ersetzen den Ausgabebefehl print() im else-Abschnitt der Hauptfunktion main().

Hinweis: Das Zusammenstellen einer panel_list (Fensterliste) erlaubt es alle Fenster auf einmal zu starten. (Ich kann dieses Verhalten bisher nicht erklären...)

Lässt man das Makro laufen, erstellt es ein sauberes, leeres Dialogfenster, das auf Widgets wartet.

Ein leeres Dialogfenster

Das Makro bis hierhin...

#! python
# -*- coding: utf-8 -*-
# (c) 2021 Your name LGPL

# imports and constants
from PySide2.QtWidgets import (QDialog)

class ControlPanel(QDialog):
    """
    docstring for ControlPanel.
    """
    def __init__(self, document, actuator):
        super(ControlPanel, self).__init__()
        self.initUI(document, actuator)

    def initUI(self, document, actuator):
        # Setting up class parameters
        # the window has 640 x 480 pixels and is centered by default
        # now make the window visible
        self.show()


def findTheDrivingConstraints(document_object):
    # search through the Objects and find the driving constraint
    driver_list = []
    for each in document_object.Objects:
        if each.Label.endswith("Driver"):
            driving_constraint = each.Name
            driver_list.append(driving_constraint)
    return driver_list

def main():
    kin_doc = App.ActiveDocument # Kinematic Document
    drivers = findTheDrivingConstraints(kin_doc)
    if len(drivers) < 1:
        print("No driver found!")
    else:
        panel_list = []
        for each_driver in drivers:
            panel = ControlPanel(kin_doc, each_driver)
            panel_list.append(panel)
        panel.exec_()

if __name__ == "__main__":
    # This will be true only if the file is "executed"
    # but not if imported as a module
    main()

Anfang

Parameter setzen

Nun ist es Zeit die Methode initUI() zu befüllen:

...
    def initUI(self, document, actuator):
        # Setting up class parameters
        self.actuator = document.getObject(actuator)
        self.driver_type = self.getDriverType(self.actuator)
        # the window has 640 x 480 pixels and is centered by default
        # now make the window visible
        self.show()
...

self.actuator repräsentiert den Antrieb und self.driver_type Speichert ein Schlüsselwort für die Art des Antriebs. Letztere sorgt dafür, dass jeder Randbedingung die richtige Eigenschaft zugeordnet wird.

Methode getDriverType()

Für spätere Verwendung muss die Art des Antriebs ermittelt werden, Winkel, Abstand oder Länge (Angle, Distance, Length), daher muss eine Methode namens getDriverType() (Finde den Antriebstyp) definiert werden.

...
    def getDriverType(self, constraint):
        ANGLE_CONSTRAINTS = [
            "PlaneCoincident",
            "AxialAlignment",
            "PlaneAlignment"
            ] # Angel constraints that use negative values, too
        ANGLE_DEG_CONSTRAINTS = [
            "Angle",
            ] # Angel constraints that use positive values only
        DISTANCE_CONSTRAINTS = [
            "PointDistance",
            "PointsDistance"
            ] # Lenght constraints that use positive values only
        if constraint.ConstraintType in ANGLE_CONSTRAINTS:
            return "Angle"
        elif constraint.ConstraintType in ANGLE_DEG_CONSTRAINTS:
            return "AnglePos"
        elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
            return "Distance"
        else:
            return "Length"
...

Diese Methode überprüft, ob die Art der gegebenen Randbedingung in einer der Listen zu finden ist, und gibt zurück, welche Art Maß zu steuern ist

Es wird angenommen, dass der Antrieb im Kinematikdokument korrekt markiert wurde und funktioniert, wenn er von Hand geändert wird. In diesem Falle ist es nicht nötig geometrische Randbedingungen wie Colinear oder PointsCoincidence herauszufiltern (aber hier wäre die Stelle es zu tun...)

Fenstereigenschaften

Die Fenstergröße wird über seine Größt- und Kleinstmaße festgelegt. Verwendet man jeweils dieselben Werte, wird eine konstante Größe festgelegt

Im Titel findet sich der Name des Antriebs und ob es sich um einen Winkel, einen Abstand oder eine Länge handelt. Schließlich wird das Fenster angewiesen über allen anderen Fenstern (sichtbar) zu bleiben.

...
        # the window has 640 x 480 pixels and is centered by default
        #- set window dimensions
        self.setMaximumWidth(400)
        self.setMaximumHeight(200)
        self.setMinimumWidth(400)
        self.setMinimumHeight(200)
        self.setWindowTitle(self.actuator.Label + ": " + self.driver_type)
        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
        # now make the window visible
...

Anfang

Weitere Parameter setzen

Im nächsten Schritt wird der Wert des Antriebs ausgelesen und die Start- und Endwerte abhängig von der Antriebsart gesetzt.

Ein Abstand kann nicht negativ sein und exakt null verwirrt den Gleichungslöser, daher wird der Startwert auf 0,001 gesetzt. Winkel akzeptieren negative Werte und erhalten symmetrische Werte. (Ob Längen negative Werte akzeptieren, muss noch abschließend festgestellt werden...)

Die Einheiten müssen gespeichert werden, bis die Werte am Ende an die Eigenschaften der Randbedingungen zurückgegeben werden. Abstände und Längen benötigen Werte mit Einheiten.

Der Umgang mit Einheiten und die Darstellung der Werte als Zeichenketten in einigen Widgets erfordert es häufig Zahlen in Zeichenketten und wieder zurück zu verwandeln.

Um die Parameter zu vervollständigen, wird ein Wert für die Anzahl von Schritten vorgegeben, die berechnet werden solle, wenn die Bewegung automatisch erfolgt; und wenn der Umschalter self.sequence auf True gesetzt ist, wird bei jedem Schritt der Bewegung ein Bild aufgenommen.

...
        self.steps_value = 10
        self.sequence = False
        if self.driver_type == "Angle":
            self.current_value = self.actuator.Angle
            self.start_value = (self.current_value - 15)
            self.end_value = (self.current_value + 15)
            self.unit_suffix = (" °")
        elif self.driver_type == "AnglePos":
            self.current_value = float(str(self.actuator.Angle)[:-4])
            self.start_value = 0.001 # Angle must not be <= 0 and >=180
            self.end_value = 179.999
            self.unit_suffix = (" °")
        elif self.driver_type == "Distance":
            self.current_value = float(str(self.actuator.Distance)[:-3])
            self.start_value = 0.001 # Distance must not be <= 0
            self.end_value = (self.current_value + 10)
            self.unit_suffix = (" mm")
        else:
            self.current_value = float(str(self.actuator.Offset)[:-3])
            self.start_value = (self.current_value - 10)
            self.end_value = (self.current_value + 10)
            self.unit_suffix = (" mm")
...

Anfang

Label

Nun werden drei Label(-Widgets) hizugefügt, um Start-, End-, sowie den aktuellen Wert anzuzeigen

Zuerst muss die Klasse QLabel importiert werden, d. h. die Import-Liste muss, wie hier gezeigt wird, ergänzt werden:

from PySide2.QtWidgets import (QDialog, QLabel)

Zurück in der Methode initUI() fügt man folgendes ein:

...
        # create some labels
        self.label_start = QLabel("", self)
        self.label_start.setFont("osifont") # set to a non-proportional font
        self.label_start.setText(str(round(self.start_value, 1)) + self.unit_suffix)
        self.label_start.setGeometry(QtCore.QRect(30, 15, 60, 25))

        self.label_end = QLabel("", self)
        self.label_end.setFont("osifont")
        self.label_end.setText(str(round(self.end_value, 1)) + self.unit_suffix)
        self.label_end.setGeometry(QtCore.QRect(320, 15, 60, 25))

        self.label_current = QLabel("", self)
        self.label_current.setFont("osifont")
        self.label_current.setText("Current value: " + str(round(self.current_value, 1)) + self.unit_suffix)
        self.label_current.setGeometry(QtCore.QRect(130, 15, 150, 25))
...

Die Platzierung erfolgt mit der ererbten Methode setGeometry(). In diesem Falle wird die Beschreibung eines Rechtecks verwendet (X-Position, Y-Position, Breite, Höhe)

Die ersten und dritten Zeilen könnten zusammengefasst werden, wird aber aus Gründen der Übersichtlichkeit nicht empfohlen.

self.label_end = QLabel((str(round(self.end_value, 1)) + self.unit_suffix), self)

Startet man das Makro aus einem Kinematikdokument heraus, erhält man ein Dialogfenster wie dieses:

Ein Dialogfenster, das den Startwert, den aktuellen Wert und den Endwert anzeigt

Das Dialogfenster zeigt in der Titelleiste das Label der Randbedingung und die Art des Antriebs an, sowie den Startwert, den aktuellen Wert und den Endwert in der ersten Zeile des Hauptbereiches

Das Makro bis hierhin...

#! python
# -*- coding: utf-8 -*-
# (c) 2021 Your name LGPL

# imports and constants
from PySide2.QtWidgets import (QDialog, QLabel)

class ControlPanel(QDialog):
    """
    docstring for ControlPanel.
    """
    def __init__(self, document, actuator):
        super(ControlPanel, self).__init__()
        self.initUI(document, actuator)

    def initUI(self, document, actuator):
        # Setting up class parameters
        self.actuator = document.getObject(actuator)
        self.driver_type = self.getDriverType(self.actuator)
        self.steps_value = 10
        self.sequence = False
        if self.driver_type == "Angle":
            self.current_value = self.actuator.Angle
            self.start_value = (self.current_value - 15)
            self.end_value = (self.current_value + 15)
            self.unit_suffix = (" °")
        elif self.driver_type == "AnglePos":
            self.current_value = float(str(self.actuator.Angle)[:-4])
            self.start_value = 0.001 # Angle must not be <= 0 and >=180
            self.end_value = 179.999
            self.unit_suffix = (" °")
        elif self.driver_type == "Distance":
            self.current_value = float(str(self.actuator.Distance)[:-3])
            self.start_value = 0.001 # Distance must not be <= 0
            self.end_value = (self.current_value + 10)
            self.unit_suffix = (" mm")
        else:
            self.current_value = float(str(self.actuator.Offset)[:-3])
            self.start_value = (self.current_value - 10)
            self.end_value = (self.current_value + 10)
            self.unit_suffix = (" mm")

        # the window has 640 x 480 pixels and is centered by default
        #- set window dimensions
        self.setMaximumWidth(400)
        self.setMaximumHeight(200)
        self.setMinimumWidth(400)
        self.setMinimumHeight(200)
        self.setWindowTitle(self.actuator.Label + ": " + self.driver_type)
        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)

        # create some labels
        self.label_start = QLabel("", self)
        self.label_start.setFont("osifont") # set to a non-proportional font
        self.label_start.setText(str(round(self.start_value, 1)) + self.unit_suffix)
        self.label_start.setGeometry(QtCore.QRect(30, 15, 60, 25))

        self.label_end = QLabel("", self)
        self.label_end.setFont("osifont")
        self.label_end.setText(str(round(self.end_value, 1)) + self.unit_suffix)
        self.label_end.setGeometry(QtCore.QRect(320, 15, 60, 25))

        self.label_current = QLabel("", self)
        self.label_current.setFont("osifont")
        self.label_current.setText("Current value: " + str(round(self.current_value, 1)) + self.unit_suffix)
        self.label_current.setGeometry(QtCore.QRect(130, 15, 150, 25))

        # now make the window visible
        self.show()

    def getDriverType(self, constraint):
        ANGLE_CONSTRAINTS = [
            "PlaneCoincident",
            "AxialAlignment",
            "PlaneAlignment"
            ] # Angel constraints that use negative values, too
        ANGLE_DEG_CONSTRAINTS = [
            "Angle",
            ] # Angel constraints that use positive values only
        DISTANCE_CONSTRAINTS = [
            "PointDistance",
            "PointsDistance"
            ]
        if constraint.ConstraintType in ANGLE_CONSTRAINTS:
            return "Angle"
        elif constraint.ConstraintType in ANGLE_DEG_CONSTRAINTS:
            return "AnglePos"
        elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
            return "Distance"
        else:
            return "Length"

# End of ControlPanel()
# Main section below:

def findTheDrivingConstraints(document_object):
    # search through the Objects and find the driving constraint
    driver_list = []
    for each in document_object.Objects:
        if each.Label.endswith("Driver"):
            driving_constraint = each.Name
            driver_list.append(driving_constraint)
    return driver_list

def main():
    kin_doc = App.ActiveDocument # Kinematic Document
    drivers = findTheDrivingConstraints(kin_doc)
    if len(drivers) < 1:
        print("No driver found!")
    else:
        panel_list = []
        for each_driver in drivers:
            panel = ControlPanel(kin_doc, each_driver)
            panel_list.append(panel)
        panel.exec_()

if __name__ == "__main__":
    # This will be true only if the file is "executed"
    # but not if imported as a module
    main()

Anfang

Schieberegler

Um den aktuellen Wert auf jede mögliche Zahl zwischen Start- und Endwert einzustellen, würde ein Schieberegler passen.

Zuerst muss die Klasse QSlider importiert werden, d. h. die Import-Liste muss, wie hier gezeigt wird, ergänzt werden:

from PySide2.QtWidgets import (QDialog, QLabel, QSlider)

Zurück in der Methode initUI() fügt man folgendes direkt nach dem Labels-Abschnitt ein:

...
        # Horizontal slider
        self.actuator_slider = QSlider(self)                             # create horizontalSlider
        self.actuator_slider.setOrientation(QtCore.Qt.Horizontal)        # orientation horizontal
        self.actuator_slider.setGeometry(QtCore.QRect(30, 50, 330, 25))  # position coordinates
        self.actuator_slider.setObjectName("horizontalSlider")           # object name
        self.actuator_slider.setInvertedAppearance(False)                # default: right to left
        self.actuator_slider.setRange(0, 100)                            # default: (0, 99)
        self.actuator_slider.setValue(self.current_value / self.stepRatio())
        self.actuator_slider.valueChanged.connect(self.onActuatorSlider)
...

Der Schieberknopf wird mit der Methode setValue() platziert. Sein Wert muss aus dem aktuellen Wert und dem Schrittverhältnis berechnet werden. Das Schrittverhältnis muss immer berechnet werden, wenn sich der Start- oder Endwert ändert, daher wird eine weitere Methode hinter der Methode getDriverType() eingefügt.

Ein Verhältnis zu verwenden anstatt die Kleinst- und Größtwerte des Schiebereglers zu verändern, hat den Vorteil einer feineren Auflösung für kleinere Werte.

...
    def stepRatio(self):
        ratio = (self.end_value - self.start_value) / 100
        return ratio
...

Und nach diesem kommt eine weitere Methode, die festlegt, was zu tun ist, wenn sich die Position des Schiebers bzw. der Wert des Schiebereglers ändert. Die Methode onActuatorSlider() wird von der Methode connect() aufgerufen, die auch den Wert des Schhiebereglers als Variable übergibt.

Sie berechnet den aktuellen Wert erneut aus der Schieberposition, überschreibt den Text des Labels self.label_current und ändert die Eigenschaft der Randbedingung unter Beachtung der Art des Antriebs.

Wird der Befehl "asm3CmdQuickSolve" gestartet, beginnt der Gleichnungslöser die Bestandteile der Baugruppe dem geänderten Wert entsprechend neu anzuordnen.

...
    def onActuatorSlider(self, slider_value):
        self.current_value = slider_value * self.stepRatio() + self.start_value
        if self.driver_type == "Angle":
            self.actuator.Angle = self.current_value
        elif self.driver_type == "AnglePos":
            self.actuator.Angle = self.current_value
        elif self.driver_type == "Distance":
            self.actuator.Distance = self.current_value
        else:
            self.actuator.Offset = self.current_value
        self.label_current.setText("Current value: " + str(round(self.current_value, 1)) + self.unit_suffix)
        Gui.runCommand("asm3CmdQuickSolve", 0)
...

Das Dialogfenster mit dem Schieberegler sollte so aussehen und ist jetzt bereit eine Bewegung zu steuern.

Zwei Dialogfenster mit Schieberegler

Dialogfenster mit dem hinzugefügten Schieberegler, eins für einen Winkelantrieb (Drehantrieb) und eins für einen (Abstands-/) Linearantrieb

Es kann ein Dialogfenster für jedes geöffnete Dokument gestartet werden; sie beeinflussen sich nicht gegenseitig.

Anfang

Texteingabefelder

Um Start- und Endwerte zu ändern kann man ein Line-Edit-Widget verwenden.

Zuerst muss die Klasse QLineEdit importiert werden, d. h. die Import-Liste muss, wie hier gezeigt wird, ergänzt werden:

from PySide2.QtWidgets import (QDialog, QLabel, QSlider, QLineEdit)

Zurück in der Methode initUI() fügt man folgendes zwischen Labels- und Slider-Abschnitt ein:

...
        # text input field - Start value
        self.entry_start = QLineEdit(self)
        self.entry_start.setText(str(round(self.start_value, 1)))
        self.entry_start.setGeometry(QtCore.QRect(30, 80, 50, 25))
        self.entry_start.textChanged[str].connect(self.onEntryStart)

        # text input field - End value
        self.entry_end = QLineEdit(self)
        self.entry_end.setText(str(round(self.end_value, 1)))
        self.entry_end.setGeometry(QtCore.QRect(320, 80, 50, 25))
        self.entry_end.textChanged[str].connect(self.onEntryEnd)
...

Die Eingabefelder zeigen die voreingestellten Start- und Endwerte an. Sie sind aber erst komplett, wenn auch Methoden hinzugefügt werden, die die geänderten Werte verarbeiten. Das erledigen die Methoden self.onEntryStart() und self.onEntryEnd(), die zwischen den Methoden self.stepRatio() und self.onActuatorSlider() eingefügt werden.

...
    def onEntryStart(self, new_start):
        self.start_value = float(new_start)
        self.label_start.setText(str(round(self.start_value, 1)) + self.unit_suffix)
        # Update the slider
        slider_value = ((self.current_value - self.start_value) / self.stepRatio())
        self.actuator_slider.setValue(slider_value)

    def onEntryEnd(self, new_end):
        self.end_value = float(new_end)
        self.label_end.setText(str(round(self.end_value, 1)) + self.unit_suffix)
        # Update the slider
        slider_value = ((self.current_value - self.start_value) / self.stepRatio())
        self.actuator_slider.setValue(slider_value)
...

Beide wandeln die erhaltene Zeichenkette in eine Fließkommazahl und ändern den Startwert self.start_value bzw. Endwert self.end_value und das dazugehörige Label. Danach wird der Schieber aktualisiert.

Das Dialogfenster mit den Texteingabefeldern sollte so aussehen und ist jetzt bereit den Bereich einer Bewegung zu ändern:

Zwei Dialogfenster mit Texteingabefeldern

Dialogfenster mit Texteingabefeldern, nochmals für Drehantrieb und Linearantrieb

Das Makro bis hierhin...

#! python
# -*- coding: utf-8 -*-
# (c) 2021 Your name LGPL

# imports and constants
from PySide2.QtWidgets import (QDialog, QLabel, QSlider, QLineEdit)

class ControlPanel(QDialog):
    """
    docstring for ControlPanel.
    """
    def __init__(self, document, actuator):
        super(ControlPanel, self).__init__()
        self.initUI(document, actuator)

    def initUI(self, document, actuator):
        # Setting up class parameters
        self.actuator = document.getObject(actuator)
        self.driver_type = self.getDriverType(self.actuator)
        self.steps_value = 10
        self.sequence = False
        if self.driver_type == "Angle":
            self.current_value = self.actuator.Angle
            self.start_value = (self.current_value - 15)
            self.end_value = (self.current_value + 15)
            self.unit_suffix = (" °")
        elif self.driver_type == "AnglePos":
            self.current_value = float(str(self.actuator.Angle)[:-4])
            self.start_value = 0.001 # Angle must not be <= 0 and >=180
            self.end_value = 179.999
            self.unit_suffix = (" °")
        elif self.driver_type == "Distance":
            self.current_value = float(str(self.actuator.Distance)[:-3])
            self.start_value = 0.001 # Distance must not be <= 0
            self.end_value = (self.current_value + 10)
            self.unit_suffix = (" mm")
        else:
            self.current_value = float(str(self.actuator.Offset)[:-3])
            self.start_value = (self.current_value - 10)
            self.end_value = (self.current_value + 10)
            self.unit_suffix = (" mm")

        # the window has 640 x 480 pixels and is centered by default
        #- set window dimensions
        self.setMaximumWidth(400)
        self.setMaximumHeight(200)
        self.setMinimumWidth(400)
        self.setMinimumHeight(200)
        self.setWindowTitle(self.actuator.Label + ": " + self.driver_type)
        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)

        # create some labels
        self.label_start = QLabel("", self)
        self.label_start.setFont("osifont") # set to a non-proportional font
        self.label_start.setText(str(round(self.start_value, 1)) + self.unit_suffix)
        self.label_start.setGeometry(QtCore.QRect(30, 15, 60, 25))

        self.label_end = QLabel("", self)
        self.label_end.setFont("osifont")
        self.label_end.setText(str(round(self.end_value, 1)) + self.unit_suffix)
        self.label_end.setGeometry(QtCore.QRect(320, 15, 60, 25))

        self.label_current = QLabel("", self)
        self.label_current.setFont("osifont")
        self.label_current.setText("Current value: " + str(round(self.current_value, 1)) + self.unit_suffix)
        self.label_current.setGeometry(QtCore.QRect(130, 15, 150, 25))

        # create some input elements

        # text input field - Start value
        self.entry_start = QLineEdit(self)
        self.entry_start.setText(str(round(self.start_value, 1)))
        self.entry_start.setGeometry(QtCore.QRect(30, 80, 50, 25))
        self.entry_start.textChanged[str].connect(self.onEntryStart)

        # text input field - End value
        self.entry_end = QLineEdit(self)
        self.entry_end.setText(str(round(self.end_value, 1)))
        self.entry_end.setGeometry(QtCore.QRect(320, 80, 50, 25))
        self.entry_end.textChanged[str].connect(self.onEntryEnd)

        # Horizontal slider
        self.actuator_slider = QSlider(self)                             # create horizontalSlider
        self.actuator_slider.setOrientation(QtCore.Qt.Horizontal)        # orientation horizontal
        self.actuator_slider.setGeometry(QtCore.QRect(30, 50, 330, 25))  # position coordinates
        self.actuator_slider.setObjectName("horizontalSlider")           # object name
        self.actuator_slider.setInvertedAppearance(False)                # default: right to left
        self.actuator_slider.setRange(0, 100)                            # default: (0, 99)
        self.actuator_slider.setValue(self.current_value / self.stepRatio())
        self.actuator_slider.valueChanged.connect(self.onActuatorSlider)

        # now make the window visible
        self.show()

    def getDriverType(self, constraint):
        ANGLE_CONSTRAINTS = [
            "PlaneCoincident",
            "AxialAlignment",
            "PlaneAlignment"
            ] # Angel constraints that use negative values, too
        ANGLE_DEG_CONSTRAINTS = [
            "Angle",
            ] # Angel constraints that use positive values only
        DISTANCE_CONSTRAINTS = [
            "PointDistance",
            "PointsDistance"
            ] # Lenght constraints that use positive values only
        if constraint.ConstraintType in ANGLE_CONSTRAINTS:
            return "Angle"
        elif constraint.ConstraintType in ANGLE_DEG_CONSTRAINTS:
            return "AnglePos"
        elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
            return "Distance"
        else:
            return "Length"

    def stepRatio(self):
        ratio = (self.end_value - self.start_value) / 100
        return ratio

    def onEntryStart(self, new_start):
        self.start_value = float(new_start)
        self.label_start.setText(str(round(self.start_value, 1)) + self.unit_suffix)
        # Update the slider
        slider_value = ((self.current_value - self.start_value) / self.stepRatio())
        self.actuator_slider.setValue(slider_value)

    def onEntryEnd(self, new_end):
        self.end_value = float(new_end)
        self.label_end.setText(str(round(self.end_value, 1)) + self.unit_suffix)
        # Update the slider
        slider_value = ((self.current_value - self.start_value) / self.stepRatio())
        self.actuator_slider.setValue(slider_value)

    def onActuatorSlider(self, slider_value):
        self.current_value = slider_value * self.stepRatio() + self.start_value
        if self.driver_type == "Angle":
            self.actuator.Angle = self.current_value
        elif self.driver_type == "AnglePos":
            self.actuator.Angle = self.current_value
        elif self.driver_type == "Distance":
            self.actuator.Distance = self.current_value
        else:
            self.actuator.Offset = self.current_value
        self.label_current.setText("Current value: " + str(round(self.current_value, 1)) + self.unit_suffix)
        Gui.runCommand("asm3CmdQuickSolve", 0)
        print(slider_value, self.current_value)

# End of ControlPanel()
# Main section below:

def findTheDrivingConstraints(document_object):
    # search through the Objects and find the driving constraint
    driver_list = []
    for each in document_object.Objects:
        if each.Label.endswith("Driver"):
            driving_constraint = each.Name
            driver_list.append(driving_constraint)
    return driver_list

def main():
    kin_doc = App.ActiveDocument # Kinematic Document
    drivers = findTheDrivingConstraints(kin_doc)
    if len(drivers) < 1:
        print("No driver found!")
    else:
        panel_list = []
        for each_driver in drivers:
            panel = ControlPanel(kin_doc, each_driver)
            panel_list.append(panel)
        panel.exec_()

if __name__ == "__main__":
    # This will be true only if the file is "executed"
    # but not if imported as a module
    main()

Anfang

Bewegung

um einen Zusammenbau in Bewegung zu setzen benötigt man:

  • Schaltflächen, die eine Bewegung in die gewünschte Richtung auslösen.
  • Ein Eingabefeld, um die Anzahl der schritte zu verändern, für schnellere oder fließendere Bewegungen.
  • Ein Optionsfeld (Checkbox), um das Erstellen einer Bildfolge zu aktivieren.

Vorwärts- und Rückwärts-Schaltflächen

Damit sich die Bauteile automatisch bewegen, werden zwei Schaltflächen benötigt, die die Bewegungen auslösen, eine in Richtung der Startposition und eine in Richtung der Endposition. Diese beiden und eine zum Schließen verwenden das Widget QPushButton.

Kleinere Baugruppen werde etwas zu schnell berechnet und zeigen eher Sprünge anstatt einer fließenden Bewegung. Um sie zu verlangsamen wird die Methode sleep() des Moduls time verwendet, das zuerst importiert werden muss.

Ein weiterer Import und ein weiteres Widget:

import time
from PySide2.QtWidgets import (QDialog, QLabel, QSlider, QLineEdit, QPushButton)

Zurück in der Methode initUI() fügt man folgendes nach dem Slider-Abschnitt ein:

...
        # forward button
        self.forward_button = QPushButton(">->", self)
        self.forward_button.setGeometry(QtCore.QRect(240, 80, 50, 25))
        self.forward_button.setAutoDefault(False)
        self.forward_button.clicked.connect(self.onForward)
        # backward button
        self.backward_button = QPushButton("<-<", self)
        self.backward_button.setGeometry(QtCore.QRect(100, 80, 50, 25))
        self.backward_button.setAutoDefault(False)
        self.backward_button.clicked.connect(self.onBackward)
        # close button
        self.close_button = QPushButton("Close window", self)
        self.close_button.setGeometry(QtCore.QRect(120, 160, 130, 25))
        self.close_button.setAutoDefault(False)
        self.close_button.clicked.connect(self.onClose)
...

Die Methoden, die gedrückte Schaltflächen bearbeiten sind self.onForward(), self.onBackward(), und self.onClose(). Sie werden nach der Methode onActuatorSlider() eingefügt.

...
    def onForward(self):
        steps_left = self.steps_value
        print(self.steps_value)
        step = ((self.end_value - self.current_value) / steps_left)
        while steps_left > 0:
            self.current_value += step
            slider_value = ((self.current_value - self.start_value) / self.stepRatio())
            self.actuator_slider.setValue(slider_value)
            time.sleep(0.2)
            steps_left -= 1
        self.actuator_slider.setValue(100)

    def onBackward(self):
        steps_left = self.steps_value
        step = ((self.current_value - self.start_value) / steps_left)
        while steps_left > 0:
            self.current_value -= step
            slider_value = ((self.current_value - self.start_value) / self.stepRatio())
            self.actuator_slider.setValue(slider_value)
            time.sleep(0.2)
            steps_left -= 1
        self.actuator_slider.setValue(0)

    def onClose(self):
        self.result = "Closed"
        self.close()
...

Die Methode self.onClose() ruft die ererbte Methode self.close() auf, die nur das Dialogfenster schließt und damit das Makro beendet.

Beide Methoden self.onForward() und self.onBackward() zählen die verbleibenden Schritte, die noch zu gehen sind, bis man die gewünschte Position erreicht und berechnen die Schrittlänge entsprechend der Anzahl der Schritte. Zu diesem Zeitpunkt wird die voreingestellte Anzahl von zehn Schritten beibehalten.

Jede Runde in der While-Schleife erhöht/verringert den aktuellen Wert und aktualisiert den Schieber, wodurch im Hintergrund onActuatorSlider() ausgelöst wird (siehe Absatz Schieberegler).

Wenn keine Schritte mehr übrig sind, wird der Schieber auf die erste bzw. letzte Position gesetzt, nur für den Fall, dass Rundungsfehler aufgetreten sind.

Das Dialogfenster mit den Schalflächen sollte so aussehen und kann jetzt einen Zusammenbau mit 10 Schritten auf die gewünschte Position bewegen:

Dialogfenster mit Schaltflächen

Dialogfenster mit Schaltflächen

Das Makro bis hierhin...

#! python
# -*- coding: utf-8 -*-
# (c) 2021 Your name LGPL

# imports and constants
import time
from PySide2.QtWidgets import (QDialog, QLabel, QSlider, QLineEdit, QPushButton)

class ControlPanel(QDialog):
    """
    docstring for ControlPanel.
    """
    def __init__(self, document, actuator):
        super(ControlPanel, self).__init__()
        self.initUI(document, actuator)

    def initUI(self, document, actuator):
        # Setting up class parameters
        self.actuator = document.getObject(actuator)
        self.driver_type = self.getDriverType(self.actuator)
        self.steps_value = 10
        self.sequence = False
        if self.driver_type == "Angle":
            self.current_value = self.actuator.Angle
            self.start_value = (self.current_value - 15)
            self.end_value = (self.current_value + 15)
            self.unit_suffix = (" °")
        elif self.driver_type == "AnglePos":
            self.current_value = float(str(self.actuator.Angle)[:-4])
            self.start_value = 0.001 # Angle must not be <= 0 and >=180
            self.end_value = 179.999
            self.unit_suffix = (" °")
        elif self.driver_type == "Distance":
            self.current_value = float(str(self.actuator.Distance)[:-3])
            self.start_value = 0.001 # Distance must not be <= 0
            self.end_value = (self.current_value + 10)
            self.unit_suffix = (" mm")
        else:
            self.current_value = float(str(self.actuator.Offset)[:-3])
            self.start_value = (self.current_value - 10)
            self.end_value = (self.current_value + 10)
            self.unit_suffix = (" mm")

        # the window has 640 x 480 pixels and is centered by default
        #- set window dimensions
        self.setMaximumWidth(400)
        self.setMaximumHeight(200)
        self.setMinimumWidth(400)
        self.setMinimumHeight(200)
        self.setWindowTitle(self.actuator.Label + ": " + self.driver_type)
        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)

        # create some labels
        self.label_start = QLabel("", self)
        self.label_start.setFont("osifont") # set to a non-proportional font
        self.label_start.setText(str(round(self.start_value, 1)) + self.unit_suffix)
        self.label_start.setGeometry(QtCore.QRect(30, 15, 60, 25))

        self.label_end = QLabel("", self)
        self.label_end.setFont("osifont")
        self.label_end.setText(str(round(self.end_value, 1)) + self.unit_suffix)
        self.label_end.setGeometry(QtCore.QRect(320, 15, 60, 25))

        self.label_current = QLabel("", self)
        self.label_current.setFont("osifont")
        self.label_current.setText("Current value: " + str(round(self.current_value, 1)) + self.unit_suffix)
        self.label_current.setGeometry(QtCore.QRect(130, 15, 150, 25))

        # create some input elements

        # text input field - Start value
        self.entry_start = QLineEdit(self)
        self.entry_start.setText(str(round(self.start_value, 1)))
        self.entry_start.setGeometry(QtCore.QRect(30, 80, 50, 25))
        self.entry_start.textChanged[str].connect(self.onEntryStart)

        # text input field - End value
        self.entry_end = QLineEdit(self)
        self.entry_end.setText(str(round(self.end_value, 1)))
        self.entry_end.setGeometry(QtCore.QRect(320, 80, 50, 25))
        self.entry_end.textChanged[str].connect(self.onEntryEnd)

        # Horizontal slider
        self.actuator_slider = QSlider(self)                             # create horizontalSlider
        self.actuator_slider.setOrientation(QtCore.Qt.Horizontal)        # orientation horizontal
        self.actuator_slider.setGeometry(QtCore.QRect(30, 50, 330, 25))  # position coordinates
        self.actuator_slider.setObjectName("horizontalSlider")           # object name
        self.actuator_slider.setInvertedAppearance(False)                # default: right to left
        self.actuator_slider.setRange(0, 100)                            # default: (0, 99)
        self.actuator_slider.setValue(self.current_value / self.stepRatio())
        self.actuator_slider.valueChanged.connect(self.onActuatorSlider)

        # forward button
        self.forward_button = QPushButton(">->", self)
        self.forward_button.setGeometry(QtCore.QRect(240, 80, 50, 25))
        self.forward_button.setAutoDefault(False)
        self.forward_button.clicked.connect(self.onForward)
        # backward button
        self.backward_button = QPushButton("<-<", self)
        self.backward_button.setGeometry(QtCore.QRect(100, 80, 50, 25))
        self.backward_button.setAutoDefault(False)
        self.backward_button.clicked.connect(self.onBackward)
        # close button
        self.close_button = QPushButton("Close window", self)
        self.close_button.setGeometry(QtCore.QRect(120, 160, 130, 25))
        self.close_button.setAutoDefault(False)
        self.close_button.clicked.connect(self.onClose)

        # now make the window visible
        self.show()

    def getDriverType(self, constraint):
        ANGLE_CONSTRAINTS = [
            "PlaneCoincident",
            "AxialAlignment",
            "PlaneAlignment"
            ] # Angel constraints that use negative values, too
        ANGLE_DEG_CONSTRAINTS = [
            "Angle",
            ] # Angel constraints that use positive values only
        DISTANCE_CONSTRAINTS = [
            "PointDistance",
            "PointsDistance"
            ] # Lenght constraints that use positive values only
        if constraint.ConstraintType in ANGLE_CONSTRAINTS:
            return "Angle"
        elif constraint.ConstraintType in ANGLE_DEG_CONSTRAINTS:
            return "AnglePos"
        elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
            return "Distance"
        else:
            return "Length"

    def stepRatio(self):
        ratio = (self.end_value - self.start_value) / 100
        return ratio

    def onEntryStart(self, new_start):
        self.start_value = float(new_start)
        self.label_start.setText(str(round(self.start_value, 1)) + self.unit_suffix)
        # Update the slider
        slider_value = ((self.current_value - self.start_value) / self.stepRatio())
        self.actuator_slider.setValue(slider_value)

    def onEntryEnd(self, new_end):
        self.end_value = float(new_end)
        self.label_end.setText(str(round(self.end_value, 1)) + self.unit_suffix)
        # Update the slider
        slider_value = ((self.current_value - self.start_value) / self.stepRatio())
        self.actuator_slider.setValue(slider_value)

    def onActuatorSlider(self, slider_value):
        self.current_value = slider_value * self.stepRatio() + self.start_value
        if self.driver_type == "Angle":
            self.actuator.Angle = self.current_value
        elif self.driver_type == "AnglePos":
            self.actuator.Angle = self.current_value
        elif self.driver_type == "Distance":
            self.actuator.Distance = self.current_value
        else:
            self.actuator.Offset = self.current_value
        self.label_current.setText("Current value: " + str(round(self.current_value, 1)) + self.unit_suffix)
        FreeCADGui.updateGui() # screen update between steps
        Gui.runCommand("asm3CmdQuickSolve", 0)

    def onForward(self):
        steps_left = self.steps_value
        print(self.steps_value)
        step = ((self.end_value - self.current_value) / steps_left)
        while steps_left > 0:
            self.current_value += step
            slider_value = ((self.current_value - self.start_value) / self.stepRatio())
            self.actuator_slider.setValue(slider_value)
            time.sleep(0.2)
            steps_left -= 1
        self.actuator_slider.setValue(100)

    def onBackward(self):
        steps_left = self.steps_value
        step = ((self.current_value - self.start_value) / steps_left)
        while steps_left > 0:
            self.current_value -= step
            slider_value = ((self.current_value - self.start_value) / self.stepRatio())
            self.actuator_slider.setValue(slider_value)
            time.sleep(0.2)
            steps_left -= 1
        self.actuator_slider.setValue(0)

    def onClose(self):
        self.result = "Closed"
        self.close()

# End of ControlPanel()
# Main section below:

def findTheDrivingConstraints(document_object):
    # search through the Objects and find the driving constraint
    driver_list = []
    for each in document_object.Objects:
        if each.Label.endswith("Driver"):
            driving_constraint = each.Name
            driver_list.append(driving_constraint)
    return driver_list

def main():
    kin_doc = App.ActiveDocument # Kinematic Document
    drivers = findTheDrivingConstraints(kin_doc)
    if len(drivers) < 1:
        print("No driver found!")
    else:
        panel_list = []
        for each_driver in drivers:
            panel = ControlPanel(kin_doc, each_driver)
            panel_list.append(panel)
        panel.exec_()

if __name__ == "__main__":
    # This will be true only if the file is "executed"
    # but not if imported as a module
    main()

Anfang

Anzahl der Schritte

Die Voreinstellung ist so gewählt, dass man einen schnellen Eindruck erhält, ob sich der Zusammenbau wie erwartet bewegt, ohne zu viel Rechenzeit zu verbrauchen.

Wenn die Bauteile eher springen als sich fließend zu bewegen, oder wenn winkelbasierte Antriebe Problemen bereiten, wenn der Unterschied zwischen zwei Winkeln zu groß ist, dann kann beides durch das Erhöhen der Anzahl der Schritte behoben werden.

Und ein weiteres Line-Edit-Widget wird verwendet, um die Anzahl der Schritte zu ändern (eingesetzt hinter den schon vorhandenen Line-Edit-Widgets):

...
        # text input field - number of steps
        self.entry_steps = QLineEdit(self)
        self.entry_steps.setText(str(int(self.steps_value)))
        self.entry_steps.setGeometry(QtCore.QRect(180, 80, 50, 25))
        self.entry_steps.textChanged[str].connect(self.onEntrySteps)
...

Die zugehörige Methode self.onEntrySteps() füllt nur den Parameter self.step_value mit dem eingegebenen Wert. Sie wird nach der Methode onEntryEnd() eingefügt.

...
    def onEntrySteps(self, new_steps):
        self.steps_value = int(new_steps)
...

Das Dialogfenster, das in der Lage ist, die Anzahl der Schritte zu ändern sollte so aussehen:

Dialogfenster mit einem weiteren Texteingabefeld

Dialogfenster mit einem weiteren Texteingabefeld

Anfang

Bildfolge

Sobald die Bewegung der Bauteile den Erwartungen entspricht, kann bei jedem Schritt ein Bild aufgenommen werden. Aus der entstandenen Bildfolge kann eine kurze GIF-Animation erstellt werden.

Um diese Funktionalität einzubauen, wird das Widget QCheckBox und ein Verzeichnis zum Speichern der Bilder gebraucht.

Ein weiterer Import und ein weiteres Widget:

import time
from PySide2.QtWidgets import (QDialog, QLabel, QSlider, QLineEdit, QPushButton, QCheckBox)

Zurück in der Methode initUI() fügt man folgendes nach dem Slider-Abschnitt ein:

...
        # output check box
        self.output_check = QCheckBox(self)
        self.output_check.setGeometry(QtCore.QRect(40, 120, 300, 25))
        self.output_check.setChecked(False)
        self.output_check.setText("Check to record an image sequence")
        self.output_check.setObjectName("checkBoxOutput")
        self.output_check.clicked.connect(self.onOutputClicked)
...

Die Methode onOutputClicked() synchronisiert den Parameter self.sequence und die Anzeige des Aktivierungshäkchens.

...
    def onOutputClicked(self):
        if self.sequence == True:
            self.sequence = False
            self.output_check.setChecked(False)
        else:
            self.sequence = True
            self.output_check.setChecked(True)
...

Um die Ausgabeparameter festzulegen wird die Methode output() verwendet:

...
    def output(self, counter):
        if (self.sequence == True):
            image_path = ".../FreeCAD/ScreenShots/Sequence"
            file_tag = ".png"
            height = 640
            width = 480
            background = "Transparent"
            # dealing with leading zeros
            if (counter > 999) or (counter < 0):
                print("Out of Range")
            elif (counter < 10):
                number = "00" + str(counter)
            elif (counter < 100):
                number = "0" + str(counter)
            else:
                number = str(counter)
            # Screen shot
            Gui.activeDocument().activeView().saveImage(image_path + number + file_tag, height, width, background)
...

Zuerst muss der Pfad zu den Bilddateien an das eigene Betriebssystem angepasst werden; der letzte Teil des Bildnamens bleibt ohne Zählnummer und Dateianhang. Dies muss zur Zeit von Hand gemacht werden.

Dann folgt der Dateianhang, um den Bildnamen abzuschließen, Bildhöhe und -Breite sowie eine Angabe, wie der Bildhintergrund gefüllt werden soll ("Current" (aktueller Hintergrund der 3D-Ansicht), "White" (weiß), "Black" (schwarz), oder "Transparent").

Damit immer eine dreistellige Zahl verwendet wird, müssen führende Nullen vor der Zählvariable eingefügt werden.

Am Ende wird die Skriptvariante des Befehls Std AnsichtAufnehmen verwendet, um mit Hilfe der genannten Parameter ein Bild aufzunehmen.

Es werden noch keine Bilder aufgenommen!?! Kein Problem, da diese Methode noch nicht aufgerufen wird, und daher muss noch ein Aufruf in die While-Schleife von onForward() und onBackward() eingefügt werden. Direkt nach time.sleep(0.2) wird diese Zeile eingefügt:

...
            self.output(steps_left)
...

Jetzt sollte das Makro bereit sein, einen (kinematischen) Zusammenbau zu steuern und Bilder für ein animiertes GIF-Billd aufzunehmen.

Die endgültige Version des Dialogfensters:

Fertiges Dialogfenster

Fertiges Dialogfenster

Und schließlich das ganze Makro

Nicht vergessen den Pfad in der Ausgabemethode output() anzugeben!

#! python
# -*- coding: utf-8 -*-
# (c) 2021 Your name LGPL

# imports and constants
import time
from PySide2.QtWidgets import (QDialog, QLabel, QSlider, QLineEdit, QPushButton, QCheckBox)

class ControlPanel(QDialog):
    """
    docstring for ControlPanel.
    """
    def __init__(self, document, actuator):
        super(ControlPanel, self).__init__()
        self.initUI(document, actuator)

    def initUI(self, document, actuator):
        # Setting up class parameters
        self.actuator = document.getObject(actuator)
        self.driver_type = self.getDriverType(self.actuator)
        self.steps_value = 10
        self.sequence = False
        if self.driver_type == "Angle":
            self.current_value = self.actuator.Angle
            self.start_value = (self.current_value - 15)
            self.end_value = (self.current_value + 15)
            self.unit_suffix = (" °")
        elif self.driver_type == "AnglePos":
            self.current_value = float(str(self.actuator.Angle)[:-4])
            self.start_value = 0.001 # Angle must not be <= 0 and >=180
            self.end_value = 179.999
            self.unit_suffix = (" °")
        elif self.driver_type == "Distance":
            self.current_value = float(str(self.actuator.Distance)[:-3])
            self.start_value = 0.001 # Distance must not be <= 0
            self.end_value = (self.current_value + 10)
            self.unit_suffix = (" mm")
        else:
            self.current_value = float(str(self.actuator.Offset)[:-3])
            self.start_value = (self.current_value - 10)
            self.end_value = (self.current_value + 10)
            self.unit_suffix = (" mm")

        # the window has 640 x 480 pixels and is centered by default
        #- set window dimensions
        self.setMaximumWidth(400)
        self.setMaximumHeight(200)
        self.setMinimumWidth(400)
        self.setMinimumHeight(200)
        self.setWindowTitle(self.actuator.Label + ": " + self.driver_type)
        self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)

        # create some labels
        self.label_start = QLabel("", self)
        self.label_start.setFont("osifont") # set to a non-proportional font
        self.label_start.setText(str(round(self.start_value, 1)) + self.unit_suffix)
        self.label_start.setGeometry(QtCore.QRect(30, 15, 60, 25))

        self.label_end = QLabel("", self)
        self.label_end.setFont("osifont")
        self.label_end.setText(str(round(self.end_value, 1)) + self.unit_suffix)
        self.label_end.setGeometry(QtCore.QRect(320, 15, 60, 25))

        self.label_current = QLabel("", self)
        self.label_current.setFont("osifont")
        self.label_current.setText("Current value: " + str(round(self.current_value, 1)) + self.unit_suffix)
        self.label_current.setGeometry(QtCore.QRect(130, 15, 150, 25))

        # create some input elements

        # text input field - Start value
        self.entry_start = QLineEdit(self)
        self.entry_start.setText(str(round(self.start_value, 1)))
        self.entry_start.setGeometry(QtCore.QRect(30, 80, 50, 25))
        self.entry_start.textChanged[str].connect(self.onEntryStart)

        # text input field - End value
        self.entry_end = QLineEdit(self)
        self.entry_end.setText(str(round(self.end_value, 1)))
        self.entry_end.setGeometry(QtCore.QRect(320, 80, 50, 25))
        self.entry_end.textChanged[str].connect(self.onEntryEnd)

        # text input field - number of steps
        self.entry_steps = QLineEdit(self)
        self.entry_steps.setText(str(int(self.steps_value)))
        self.entry_steps.setGeometry(QtCore.QRect(180, 80, 50, 25))
        self.entry_steps.textChanged[str].connect(self.onEntrySteps)

        # Horizontal slider
        self.actuator_slider = QSlider(self)                             # create horizontalSlider
        self.actuator_slider.setOrientation(QtCore.Qt.Horizontal)        # orientation horizontal
        self.actuator_slider.setGeometry(QtCore.QRect(30, 50, 330, 25))  # position coordinates
        self.actuator_slider.setObjectName("horizontalSlider")           # object name
        self.actuator_slider.setInvertedAppearance(False)                # default: right to left
        self.actuator_slider.setRange(0, 100)                            # default: (0, 99)
        self.actuator_slider.setValue(self.current_value / self.stepRatio())
        self.actuator_slider.valueChanged.connect(self.onActuatorSlider)

        # output check box
        self.output_check = QCheckBox(self)
        self.output_check.setGeometry(QtCore.QRect(40, 120, 300, 25))
        self.output_check.setChecked(False)
        self.output_check.setText("Check to record an image sequence")
        self.output_check.setObjectName("checkBoxOutput")
        self.output_check.clicked.connect(self.onOutputClicked)

        # forward button
        self.forward_button = QPushButton(">->", self)
        self.forward_button.setGeometry(QtCore.QRect(240, 80, 50, 25))
        self.forward_button.setAutoDefault(False)
        self.forward_button.clicked.connect(self.onForward)
        # backward button
        self.backward_button = QPushButton("<-<", self)
        self.backward_button.setGeometry(QtCore.QRect(100, 80, 50, 25))
        self.backward_button.setAutoDefault(False)
        self.backward_button.clicked.connect(self.onBackward)
        # close button
        self.close_button = QPushButton("Close window", self)
        self.close_button.setGeometry(QtCore.QRect(120, 160, 130, 25))
        self.close_button.setAutoDefault(False)
        self.close_button.clicked.connect(self.onClose)

        # now make the window visible
        self.show()

    def getDriverType(self, constraint):
        ANGLE_CONSTRAINTS = [
            "PlaneCoincident",
            "AxialAlignment",
            "PlaneAlignment"
            ] # Angel constraints that use negative values, too
        ANGLE_DEG_CONSTRAINTS = [
            "Angle",
            ] # Angel constraints that use positive values only
        DISTANCE_CONSTRAINTS = [
            "PointDistance",
            "PointsDistance"
            ] # Lenght constraints that use positive values only
        if constraint.ConstraintType in ANGLE_CONSTRAINTS:
            return "Angle"
        elif constraint.ConstraintType in ANGLE_DEG_CONSTRAINTS:
            return "AnglePos"
        elif constraint.ConstraintType in DISTANCE_CONSTRAINTS:
            return "Distance"
        else:
            return "Length"

    def stepRatio(self):
        ratio = (self.end_value - self.start_value) / 100
        return ratio

    def onEntryStart(self, new_start):
        self.start_value = float(new_start)
        self.label_start.setText(str(round(self.start_value, 1)) + self.unit_suffix)
        # Update the slider
        slider_value = ((self.current_value - self.start_value) / self.stepRatio())
        self.actuator_slider.setValue(slider_value)

    def onEntryEnd(self, new_end):
        self.end_value = float(new_end)
        self.label_end.setText(str(round(self.end_value, 1)) + self.unit_suffix)
        # Update the slider
        slider_value = ((self.current_value - self.start_value) / self.stepRatio())
        self.actuator_slider.setValue(slider_value)

    def onEntrySteps(self, new_steps):
        self.steps_value = int(new_steps)

    def onActuatorSlider(self, slider_value):
        self.current_value = slider_value * self.stepRatio() + self.start_value
        if self.driver_type == "Angle":
            self.actuator.Angle = self.current_value
        elif self.driver_type == "AnglePos":
            self.actuator.Angle = self.current_value
        elif self.driver_type == "Distance":
            self.actuator.Distance = self.current_value
        else:
            self.actuator.Offset = self.current_value
        self.label_current.setText("Current value: " + str(round(self.current_value, 1)) + self.unit_suffix)
        FreeCADGui.updateGui() # screen update between steps
        Gui.runCommand("asm3CmdQuickSolve", 0)

    def onForward(self):
        steps_left = self.steps_value
        print(self.steps_value)
        step = ((self.end_value - self.current_value) / steps_left)
        while steps_left > 0:
            self.current_value += step
            slider_value = ((self.current_value - self.start_value) / self.stepRatio())
            self.actuator_slider.setValue(slider_value)
            time.sleep(0.2)
            self.output(steps_left)
            steps_left -= 1
        self.actuator_slider.setValue(100)

    def onBackward(self):
        steps_left = self.steps_value
        step = ((self.current_value - self.start_value) / steps_left)
        while steps_left > 0:
            self.current_value -= step
            slider_value = ((self.current_value - self.start_value) / self.stepRatio())
            self.actuator_slider.setValue(slider_value)
            time.sleep(0.2)
            self.output(steps_left)
            steps_left -= 1
        self.actuator_slider.setValue(0)

    def onClose(self):
        self.result = "Closed"
        self.close()

    def onOutputClicked(self):
        if self.sequence == True:
            self.sequence = False
            self.output_check.setChecked(False)
        else:
            self.sequence = True
            self.output_check.setChecked(True)

    def output(self, counter):
        if (self.sequence == True):
            image_path = ".../FreeCAD/ScreenShots/Sequence"
            file_tag = ".png"
            height = 640
            width = 480
            background = "Transparent"
            # dealing with leading zeros
            if (counter > 999) or (counter < 0):
                print("Out of Range")
            elif (counter < 10):
                number = "00" + str(counter)
            elif (counter < 100):
                number = "0" + str(counter)
            else:
                number = str(counter)
            # Screen shot
            Gui.activeDocument().activeView().saveImage(image_path + number + file_tag, height, width, background)

# End of ControlPanel()
# Main section below:

def findTheDrivingConstraints(document_object):
    # search through the Objects and find the driving constraint
    driver_list = []
    for each in document_object.Objects:
        if each.Label.endswith("Driver"):
            driving_constraint = each.Name
            driver_list.append(driving_constraint)
    return driver_list

def main():
    kin_doc = App.ActiveDocument # Kinematic Document
    drivers = findTheDrivingConstraints(kin_doc)
    if len(drivers) < 1:
        print("No driver found!")
    else:
        panel_list = []
        for each_driver in drivers:
            panel = ControlPanel(kin_doc, each_driver)
            panel_list.append(panel)
        panel.exec_()

if __name__ == "__main__":
    # This will be true only if the file is "executed"
    # but not if imported as a module
    main()

Anfang

Ein paar Schwachstellen

  • Die Numerierung der Bildfolge ist umgekehrt, da die Variable steps_left (verbleibende Schritte) heruntergezählt wird.
  • Das Bildverzeichnis und der Name des Bildes sind im Makro hinterlegt (hard-coded).
  • Mehrfach geöffnete Steuerungsfenster sind nicht synchronisiert.