45 from PySide2.QtWidgets
import QDialogButtonBox, QMessageBox, QTreeView, \
46 QHBoxLayout, QPushButton
47 from PySide2.QtCore
import Slot, QTimer, QObject
48 from PySide2.QtCore
import Qt
49 from PySide2.QtGui
import QStandardItemModel, QStandardItem
54 PATH_TO_ICONS = path.join(FreeCAD.getHomePath(),
"Mod",
"Animate",
"Resources",
58 PATH_TO_UI = path.join(FreeCAD.getHomePath(),
"Mod",
"Animate",
"Resources",
62 NAME_NUMBER_FORMAT =
"%05d" 65 FPS_CHUNK_CODE = b
'xfPs' 128 super(ControlPanel, self).
__init__()
166 FreeCADGui.ActiveDocument.ActiveView.setAnimationEnabled(
False)
169 if self.
form.sld_seek.value() == self.
form.sld_seek.maximum():
171 QMessageBox.warning(
None,
'Error while playing',
172 "The animation is at the end.")
178 t = self.
form.sld_seek.value() \
206 FreeCADGui.ActiveDocument.ActiveView.setAnimationEnabled(
False)
209 if self.
form.sld_seek.value() == self.
form.sld_seek.minimum():
211 QMessageBox.warning(
None,
'Error while rewinding',
212 "The animation is at the beginning.")
218 t = self.
form.sld_seek.value() \
242 FreeCADGui.ActiveDocument.ActiveView.setAnimationEnabled(
False)
245 if self.
form.sld_seek.value() == self.
form.sld_seek.maximum():
247 QMessageBox.warning(
None,
'Error while playing',
248 "The animation is at the end.")
252 elif not os.access(self.
control_proxy.ExportPath, os.W_OK | os.R_OK):
254 QMessageBox.warning(
None,
'Invalid Export Path',
255 "You don't have access to read and write\n" 256 +
"in folder specified by Export Path.\n" 257 +
"Change it to be able to record images.")
264 t = self.
form.sld_seek.value() \
279 if not os.access(self.
control_proxy.ExportPath, os.W_OK | os.R_OK):
281 QMessageBox.warning(
None,
'Invalid Export Path',
282 "You don't have access to read and write\n" 283 +
"in folder specified by Export Path.\n" 284 +
"Change it to be able to record images.")
295 except FileNotFoundError
as e:
296 QMessageBox.warning(
None,
'Export Path error', str(e))
306 QMessageBox.warning(
None,
'Export error',
307 "No sequences to export.")
321 if self.
form.sld_seek.isEnabled():
323 t = self.
form.sld_seek.value() \
373 FreeCADGui.Control.closeDialog()
382 return QDialogButtonBox.Close
440 self.
form.sld_seek.setValue(
459 pause = pause*(pause > 0)
463 self.
timer.singleShot(pause,
lambda: self.
play(next_t))
494 self.
form.sld_seek.setValue(
513 pause = pause*(pause > 0)
517 self.
timer.singleShot(pause,
lambda: self.
rewind(next_t))
547 self.
form.sld_seek.setValue(
564 self.
timer.singleShot(0,
lambda: self.
record(next_t))
581 while len(objects) > 0:
583 if obj.Proxy.__class__.__name__ ==
"TrajectoryProxy" or \
584 obj.Proxy.__class__.__name__ ==
"RobRotationProxy" or \
585 obj.Proxy.__class__.__name__ ==
"RobTranslationProxy":
588 elif obj.Proxy.__class__.__name__ ==
"RobWorldProxy":
602 while len(objects) > 0:
604 if obj.Proxy.__class__.__name__ ==
"CollisionDetectorProxy":
617 while len(objects) > 0:
619 if obj.Proxy.__class__.__name__ ==
"CollisionDetectorProxy":
629 FreeCAD.ActiveDocument.recompute()
630 FreeCADGui.updateGui()
648 FreeCADGui.ActiveDocument.ActiveView.setAnimationEnabled(
False)
649 FreeCADGui.ActiveDocument.ActiveView.saveImage(
658 None,
'Saving framerate failed',
659 "Framerate was not saved, this recorded image\n" 660 +
"sequence will have to be exported using\n" 661 +
"current Step Time to compute framerate.\n" 662 +
"Check Report View for more info.")
687 img_name = re.search(
r"(seq\d+)-(\d+)(?=\.png)", f)
688 if img_name
is not None:
691 if img_name.group(1)
not in list(sequences.keys()):
694 if int(img_name.group(2)) == 0:
695 sequences[img_name.group(1)] = 1
696 last_frame = int(img_name.group(2))
699 elif int(img_name.group(2)) == (last_frame + 1):
700 sequences[img_name.group(1)] += 1
705 sequences.pop(img_name.group(1))
708 sequences = {key: val
for key, val
in sequences.items()
if val > 1}
723 NAME, N_FRAMES = range(2)
729 self.
trv_sequences.setToolTip(
"Select a sequence to export.")
744 hdr_name = QStandardItem(
"Sequence Name")
745 model.setHorizontalHeaderItem(NAME, hdr_name)
746 hdr_frames = QStandardItem(
"# of frames")
747 hdr_frames.setTextAlignment(Qt.AlignmentFlag.AlignRight)
748 model.setHorizontalHeaderItem(N_FRAMES, hdr_frames)
751 for name, frames
in sequences.items():
752 itm_name = QStandardItem(name)
753 itm_name.setSelectable(
True)
754 itm_name.setEditable(
False)
755 itm_frames = QStandardItem(str(frames))
756 itm_frames.setSelectable(
True)
757 itm_frames.setEditable(
False)
758 itm_frames.setTextAlignment(Qt.AlignmentFlag.AlignRight)
759 model.appendRow((itm_name, itm_frames))
782 background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 783 stop: 0 #0B0, stop: 1.0 #0D0); 786 QPushButton:hover {border-color: #0D0;} 788 background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 789 stop: 0 #0C0, stop: 1.0 #0F0); 790 border-color: #0E0; color: #FFF; 792 QPushButton:pressed { 793 background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 794 stop: 0 #0F0, stop: 1.0 #0C0); 801 background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 802 stop: 0 #B00, stop: 1.0 #D00); 805 QPushButton:hover {border-color: #D00;} 807 background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 808 stop: 0 #C00, stop: 1.0 #F00); 809 border-color: #E00; color: #FFF; 811 QPushButton:pressed { 812 background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 813 stop: 0 #F00, stop: 1.0 #C00); 820 def mySelectionChanged(selected, deselected):
821 if selected.isEmpty()
and not deselected.isEmpty():
823 deselected.first().indexes()[0],
829 self.
trv_sequences.selectionModel().selectionChanged.connect(
850 image_name = selected_seq +
"-" + (NAME_NUMBER_FORMAT % 0) +
".png" 851 image_path = path.join(self.
control_proxy.ExportPath, image_name)
857 None,
'Loading framerate failed',
858 "Framerate was not loaded, this recorded image\n" 859 +
"sequence will be exported using current\n" 860 +
"Step Time: FPS = 1/(Step Time) = " 863 image_name =
'"' + path.normpath(
865 + NAME_NUMBER_FORMAT +
".png")) +
'"' 866 video_name =
'"' + path.normpath(
868 selected_seq +
".mp4")) +
'"' 871 export_command =
'ffmpeg -r ' + str(fps) +
' -i ' + image_name \
872 +
' -c:v libx264 -pix_fmt yuv420p ' + video_name
876 return_val = subprocess.call(export_command)
878 if e.errno == os.errno.ENOENT:
879 QMessageBox.warning(
None,
'FFMPEG not available',
880 "FFMPEG is necessary to export video.\n" 881 +
"Please install it")
883 QMessageBox.warning(
None,
'Something failed', str(e))
885 QMessageBox.information(
None,
'Export successful!',
886 "FFMPEG successfully converted image " 887 +
"sequence into a video.")
889 QMessageBox.warning(
None,
'FFMPEG unsuccessfull',
890 "FFMPEG failed to convert sequence into " 931 QMessageBox.information(
932 None,
"Install PyPNG",
"PyPNG is missing from your FreeCAD\n" 933 +
"Please follow these instructions to install it:\n\n" 935 +
" 1) Open a command line window with admin privileges\n" 936 +
' Press "Win + X" and "A"\n\n' 937 +
" 2) Go to the bin folder in your FreeCAD installation\n" 938 +
' Type "CD ' + FreeCAD.getHomePath() +
'bin"\n\n' 939 +
" 3) Install PyPNG\n" 940 +
' Type "python.exe -m pip install pyPNG"\n\n\n' 941 +
"Ubuntu (installed using PPA):\n" 942 +
" 1) Open a terminal window\n\n" 943 +
" 2) Install PyPNG\n" 944 +
' Type "sudo python.exe -m pip install pyPNG"\n')
980 except ModuleNotFoundError:
983 except Exception
as e:
984 FreeCAD.Console.PrintError(
985 "Unexpected error occurred while importing pyPNG - " + str(e))
988 reader = png.Reader(filename=image_path)
989 chunks = list(reader.chunks())
991 chunks.insert(1, (FPS_CHUNK_CODE, struct.pack(
"f", framerate)))
994 with open(image_path,
'wb')
as image_file:
995 png.write_chunks(image_file, chunks)
1018 except ModuleNotFoundError:
1022 reader = png.Reader(filename=image_path)
1023 chunks = list(reader.chunks())
1024 if chunks[1][0] == FPS_CHUNK_CODE:
1025 return struct.unpack(
"f", chunks[1][1])[0]
1027 FreeCAD.Console.PrintError(
"Unable to unpack a framerate.\n")
1086 fp.ViewObject.Proxy.setProperties(fp.ViewObject)
1102 if prop ==
"ExportPath" and hasattr(fp,
"ExportPath")
and \
1124 elif prop ==
"StartTime" and hasattr(fp,
"StopTime")
and \
1125 hasattr(fp,
"StepTime"):
1127 fp.StopTime = (fp.StopTime, fp.StartTime + fp.StepTime,
1130 fp.StepTime = (fp.StepTime, 0.01, fp.StopTime - fp.StartTime, 0.1)
1131 elif prop ==
"StepTime" and hasattr(fp,
"StartTime")
and \
1132 hasattr(fp,
"StopTime"):
1134 fp.StopTime = (fp.StopTime, fp.StartTime + fp.StepTime,
1137 fp.StartTime = (fp.StartTime, -float(
"inf"),
1138 fp.StopTime - fp.StepTime, 0.5)
1139 elif prop ==
"StopTime" and hasattr(fp,
"StartTime")
and \
1140 hasattr(fp,
"StepTime"):
1142 fp.StartTime = (fp.StartTime, -float(
"inf"),
1143 fp.StopTime - fp.StepTime, 0.5)
1145 fp.StepTime = (fp.StepTime, 0.01, fp.StopTime - fp.StartTime, 0.1)
1148 elif prop ==
"ExportPath":
1151 if not os.access(fp.ExportPath, os.W_OK | os.R_OK):
1152 QMessageBox.warning(
None,
'Error while setting Export Path',
1153 "You don't have access to read and write " 1154 +
"in this folder.")
1170 if not hasattr(fp,
"StartTime"):
1172 "App::PropertyFloatConstraint",
"StartTime",
"Timing",
1173 "Animation start time. \nRange is " 1174 "< - inf | Stop Time - Step Time >." 1175 ).StartTime = (0, -float(
"inf"), 9.5, 0.5)
1176 elif hasattr(fp,
"StepTime")
and hasattr(fp,
"StopTime"):
1177 fp.StartTime = (fp.StartTime, -float(
"inf"),
1178 fp.StopTime - fp.StepTime, 0.5)
1179 if not hasattr(fp,
"StepTime"):
1181 "App::PropertyFloatConstraint",
"StepTime",
"Timing",
1182 "Animation step time. \nRange is " 1183 "< 0.01 | Stop Time - Start Time >." 1184 ).StepTime = (0.5, 0.01, 10, 0.1)
1185 elif hasattr(fp,
"StartTime")
and hasattr(fp,
"StopTime"):
1186 fp.StepTime = (fp.StepTime, 0.01, fp.StopTime - fp.StartTime, 0.1)
1187 if not hasattr(fp,
"StopTime"):
1189 "App::PropertyFloatConstraint",
"StopTime",
"Timing",
1190 "Animation stop time. \nRange is " 1191 +
"< Start Time + Step Time | inf >." 1192 ).StopTime = (10, 0.5, float(
"inf"), 0.5)
1193 elif hasattr(fp,
"StartTime")
and hasattr(fp,
"StepTime"):
1194 fp.StopTime = (fp.StopTime, fp.StartTime + fp.StepTime,
1197 if not hasattr(fp,
"ExportPath"):
1199 "App::PropertyPath",
"ExportPath",
"Record & Export",
1200 "Path to a folder, where recorded rendered images will be " 1201 "saved to be converted into a video.")
1202 if not hasattr(fp,
"VideoWidth"):
1204 "App::PropertyIntegerConstraint",
"VideoWidth",
1205 "Record & Export",
"Width of the exported video in pixels.\n" 1206 +
"Range is < 32 | 7680 >.").VideoWidth = (1280, 32, 7680, 10)
1208 fp.VideoWidth = (fp.VideoWidth, 32, 7680, 10)
1209 if not hasattr(fp,
"VideoHeight"):
1211 "App::PropertyIntegerConstraint",
"VideoHeight",
1212 "Record & Export",
"Height of the exported video in pixels.\n" 1213 +
"Range is < 32 | 4320 >.").VideoHeight = (720, 32, 4320, 10)
1215 fp.VideoHeight = (fp.VideoHeight, 32, 4320, 10)
1218 import AnimateDocumentObserver
1284 if hasattr(self,
"fp"):
1286 return self.
fp.Group
1300 if hasattr(obj,
"Proxy")
and \
1301 (obj.Proxy.__class__.__name__ ==
"ServerProxy" or 1302 obj.Proxy.__class__.__name__ ==
"TrajectoryProxy" or 1303 obj.Proxy.__class__.__name__ ==
"CollisionDetectorProxy" or 1304 obj.Proxy.__class__.__name__ ==
"RobWorldProxy" or 1305 obj.Proxy.__class__.__name__ ==
"RobRotationProxy" or 1306 obj.Proxy.__class__.__name__ ==
"RobTranslationProxy"):
1319 return path.join(PATH_TO_ICONS,
"Control.png")
1331 vp.setEditorMode(
"DisplayMode", 2)
1332 vp.setEditorMode(
"Visibility", 2)
1348 FreeCADGui.Control.showTaskView()
1353 form = FreeCADGui.PySideUic.loadUi(
1354 path.join(PATH_TO_UI,
"AnimationControl.ui"))
1355 form.setWindowTitle(vp.Object.Label)
1360 FreeCADGui.Control.showDialog(self.
panel)
1361 except RuntimeError
as e:
1363 if str(e) ==
"Active task dialog found":
1364 QMessageBox.warning(
None,
1365 'Error while opening control panel',
1366 "A panel is already active on " 1367 +
"the Tasks tab of the Combo View.")
1368 FreeCADGui.Control.showTaskView()
1385 action = menu.addAction(
"Show control panel")
1386 action.triggered.connect(
lambda f=self.
doubleClicked, arg=vp: f(arg))
1422 class ControlCommand(object):
1433 return {
'Pixmap': path.join(PATH_TO_ICONS,
"ControlCmd.png"),
1434 'MenuText':
"Control",
1435 'ToolTip':
"Create Control instance."}
1446 doc = FreeCAD.ActiveDocument
1447 a = doc.addObject(
"App::DocumentObjectGroupPython",
"Control")
1464 if FreeCAD.ActiveDocument
is None:
def doubleClicked(self, vp)
Method called by FreeCAD when Control is double-clicked in the Tree View.
def play(self, t)
Method to show an animation frame at an animation time t during playing.
def getStandardButtons(self, *args)
Method to set just one button (close) to close the dialog.
def exportClicked(self)
Feedback method called when export button was clicked.
def onBeforeChange(self, fp, prop)
Method called before DocumentObjectGroupPython Control is changed.
def isAllowedAlterDocument(self)
Method to tell FreeCAD if dialog is allowed to alter a document.
def pauseClicked(self)
Feedback method called when pause button was clicked.
def addObserver()
Adds an AnimateDocumentObserver between FreeCAD's document observers safely.
def canDropObject(self, obj)
Method called by FreeCAD to ask if an object obj can be dropped into a Group.
def record(self, t)
Method to show and save an animation frame at an animation time t.
temporary_export_path
A str path to an export folder.
def claimChildren(self)
Method called by FreeCAD to retrieve assigned children.
def attach(self, vp)
Method called by FreeCAD after initialization.
def IsActive(self)
Method to specify when the toolbar button and the menu item are enabled.
def isAllowedAlterView(self)
Method to tell FreeCAD if dialog is allowed to alter a view.
def __init__(self, fp)
Initialization method for ControlProxy.
lyt_export
A QHBoxLayout with a confirm and abort buttons.
def findSequences(self, files)
Method to find sequences between files.
control_proxy
A proxy to an associated Control class.
Proxy class for a DocumentObjectGroupPython Control instance.
image_number
An int number of a next recorded image.
def __getstate__(self)
Necessary method to avoid errors when trying to save unserializable objects.
def onDocumentRestored(self, fp)
Method called when document is restored to make sure everything is as it was.
def Activated(self)
Method used as a callback when the toolbar button or the menu item is clicked.
def playClicked(self)
Feedback method called when play button was clicked.
def onChanged(self, fp, prop)
Method called after DocumentObjectGroupPython Control was changed.
def showSequences(self, sequences)
Method to show sequences to export on a dialog panel.
btn_confirm
A QPushButton to confirm sequence to export.
Class providing funcionality to a Control panel inside the TaskView.
ControlCommand class specifying Animate workbench's Control button/command.
def closeExportSubform(self)
Method used to close the part of the dialog panel used for video exporting.
def __init__(self, vp)
Initialization method for ViewProviderControlProxy.
def GetResources(self)
Method used by FreeCAD to retrieve resources to use for this command.
def __setstate__(self, state)
Necessary method to avoid errors when trying to restore unserializable objects.
Proxy class for Gui.ViewProviderDocumentObject Control.ViewObject.
def saveImage(self)
Method to save current view as a PNG image.
def recordClicked(self)
Feedback method called when record button was clicked.
def reject(self)
Feedback method called when Control panel is closing.
def rewindClicked(self)
Feedback method called when rewind button was clicked.
def showChanges(self)
Method to show changes made to objects, collisions.
def writeFramerateChunk(self, framerate, image_path)
Method to write a framerate into a PNG image as one of its chunks.
def exportAborted(self)
Feedback method called when abort button was clicked.
def sliderChanged(self)
Feedback method called when slider position is changed.
def __init__(self, control_proxy, form)
Initialization method for ControlPanel.
def isAllowedAlterSelection(self)
Method to tell FreeCAD if dialog is allowed to alter a selection.
form
A QDialog instance show in the TaskView.
def setupContextMenu(self, vp, menu)
Method called by the FreeCAD to customize a context menu for a Control.
def setProperties(self, fp)
Method to set properties during initialization or document restoration.
record_prefix
A str prefix for an image file name.
def distributeTime(self, t)
Method to distribute a time t to children Trajectories.
def setInvalidButtons(self)
Method to enable/disable buttons according to a last clicked button.
def exportConfirmed(self)
Feedback method called when confirm button was clicked.
trv_sequences
A QTreeView showing list of recorded sequences.
btn_abort
A QPushButton to abort exporting a sequence.
timer
A QTimer for timing animations.
def readFramerateChunk(self, image_path)
Method to read a framerate inserted as one of a PNG image's chunks.
bool updated
A bool - True if a property was changed by a class and not user.
def updateCollisions(self)
Method to update collisions from CollisionDetector children.
def resetCollisions(self)
Method to reset collisions from CollisionDetector children.
def setProperties(self, vp)
Method to hide unused properties.
def getIcon(self)
Method called by FreeCAD to supply an icon for the Tree View.
panel
A ControlPanel if one is active or None.
def rewind(self, t)
Method to show an animation frame at an animation time t during rewind.
last_clicked
A str showing which button was pressed last.
def installPyPNGNotice(self)
Method telling user that pyPNG library ought to be installed into FreeCAD.