Control.ControlPanel Class Reference

Class providing funcionality to a Control panel inside the TaskView. More...

Inheritance diagram for Control.ControlPanel:

Public Member Functions

def __init__ (self, control_proxy, form)
 Initialization method for ControlPanel. More...
 
def playClicked (self)
 Feedback method called when play button was clicked. More...
 
def pauseClicked (self)
 Feedback method called when pause button was clicked. More...
 
def rewindClicked (self)
 Feedback method called when rewind button was clicked. More...
 
def recordClicked (self)
 Feedback method called when record button was clicked. More...
 
def exportClicked (self)
 Feedback method called when export button was clicked. More...
 
def sliderChanged (self)
 Feedback method called when slider position is changed. More...
 
def setInvalidButtons (self)
 Method to enable/disable buttons according to a last clicked button. More...
 
def reject (self)
 Feedback method called when Control panel is closing. More...
 
def getStandardButtons (self, *args)
 Method to set just one button (close) to close the dialog. More...
 
def isAllowedAlterSelection (self)
 Method to tell FreeCAD if dialog is allowed to alter a selection. More...
 
def isAllowedAlterView (self)
 Method to tell FreeCAD if dialog is allowed to alter a view. More...
 
def isAllowedAlterDocument (self)
 Method to tell FreeCAD if dialog is allowed to alter a document. More...
 
def play (self, t)
 Method to show an animation frame at an animation time t during playing. More...
 
def rewind (self, t)
 Method to show an animation frame at an animation time t during rewind. More...
 
def record (self, t)
 Method to show and save an animation frame at an animation time t. More...
 
def distributeTime (self, t)
 Method to distribute a time t to children Trajectories. More...
 
def updateCollisions (self)
 Method to update collisions from CollisionDetector children. More...
 
def resetCollisions (self)
 Method to reset collisions from CollisionDetector children. More...
 
def showChanges (self)
 Method to show changes made to objects, collisions. More...
 
def saveImage (self)
 Method to save current view as a PNG image. More...
 
def findSequences (self, files)
 Method to find sequences between files. More...
 
def showSequences (self, sequences)
 Method to show sequences to export on a dialog panel. More...
 
def exportConfirmed (self)
 Feedback method called when confirm button was clicked. More...
 
def exportAborted (self)
 Feedback method called when abort button was clicked. More...
 
def closeExportSubform (self)
 Method used to close the part of the dialog panel used for video exporting. More...
 
def installPyPNGNotice (self)
 Method telling user that pyPNG library ought to be installed into FreeCAD. More...
 
def writeFramerateChunk (self, framerate, image_path)
 Method to write a framerate into a PNG image as one of its chunks. More...
 
def readFramerateChunk (self, image_path)
 Method to read a framerate inserted as one of a PNG image's chunks. More...
 

Public Attributes

 control_proxy
 A proxy to an associated Control class. More...
 
 form
 A QDialog instance show in the TaskView. More...
 
 timer
 A QTimer for timing animations. More...
 
 last_clicked
 A str showing which button was pressed last. More...
 
 record_prefix
 A str prefix for an image file name. More...
 
 image_number
 An int number of a next recorded image. More...
 
 trv_sequences
 A QTreeView showing list of recorded sequences. More...
 
 lyt_export
 A QHBoxLayout with a confirm and abort buttons. More...
 
 btn_confirm
 A QPushButton to confirm sequence to export. More...
 
 btn_abort
 A QPushButton to abort exporting a sequence. More...
 

Detailed Description

Class providing funcionality to a Control panel inside the TaskView.

This class enables user to play, pause, rewind, record, export and seek through an animation.

To create an instance of this class do:

form = FreeCADGui.PySideUic.loadUi(
path.join(PATH_TO_UI, "AnimationControl.ui"))
form.setWindowTitle(title)
panel = ControlPanel(fp, form)

Definition at line 83 of file Control.py.

Constructor & Destructor Documentation

◆ __init__()

def Control.ControlPanel.__init__ (   self,
  control_proxy,
  form 
)

Initialization method for ControlPanel.

A class instance is created. A proxy for an associated Control is added and the control properties are set to read-only as not to change when control panel is opened. A form and timer are assigned. Pause button is disabled as no animation is playing.

Parameters
control_proxyA proxy to a Control so properties can be set read-only.
formA Qt dialog loaded from a file.

Definition at line 127 of file Control.py.

127  def __init__(self, control_proxy, form):
128  super(ControlPanel, self).__init__()
129  self.control_proxy = control_proxy
130 
131  # Disable editing of Control properties
132  for prop in self.control_proxy.PropertiesList:
133  self.control_proxy.setEditorMode(prop, 1)
134 
135  # Add QDialog to be displayed in freeCAD
136  self.form = form
137 
138  # Connect callback functions
139  self.form.btn_play.clicked.connect(self.playClicked)
140  self.form.btn_pause.clicked.connect(self.pauseClicked)
141  self.form.btn_rewind.clicked.connect(self.rewindClicked)
142  self.form.btn_record.clicked.connect(self.recordClicked)
143  self.form.btn_export.clicked.connect(self.exportClicked)
144  self.form.sld_seek.valueChanged.connect(self.sliderChanged)
145 
146  # Create timer for the animations
147  self.timer = QTimer(self)
148 
149  # Disable pause button as animation is not running when the panel is
150  # opened
151  self.last_clicked = "pause"
152  self.setInvalidButtons()
153 

Member Function Documentation

◆ closeExportSubform()

def Control.ControlPanel.closeExportSubform (   self)

Method used to close the part of the dialog panel used for video exporting.

The QTreeView with sequence names and their numbers of frames are closed. Then ‘'Confirm’and'Abort'` buttons are removed and the rest of buttons is returned to the default state (the same as if pause button was pressed).

Definition at line 912 of file Control.py.

912  def closeExportSubform(self):
913  # Close all parts of export subform and remove them from the panel
914  self.trv_sequences.close()
915  self.form.lyt_main.removeWidget(self.trv_sequences)
916  self.btn_abort.close()
917  self.lyt_export.removeWidget(self.btn_abort)
918  self.btn_confirm.close()
919  self.lyt_export.removeWidget(self.btn_confirm)
920  self.form.lyt_main.removeItem(self.lyt_export)
921  self.last_clicked = "pause"
922  self.setInvalidButtons()
923 

◆ distributeTime()

def Control.ControlPanel.distributeTime (   self,
  t 
)

Method to distribute a time t to children Trajectories.

List of children is loaded. If a child is Trajectory, the time is set to it and its children are added to the list.

Parameters
tA time to distribute to all child Trajectories.

Definition at line 575 of file Control.py.

575  def distributeTime(self, t):
576  # Load list of objects inside Control group
577  objects = self.control_proxy.Group
578 
579  # Go through them, their children and update time,
580  # if they are Trajectories
581  while len(objects) > 0:
582  obj = objects.pop(0)
583  if obj.Proxy.__class__.__name__ == "TrajectoryProxy" or \
584  obj.Proxy.__class__.__name__ == "RobRotationProxy" or \
585  obj.Proxy.__class__.__name__ == "RobTranslationProxy":
586  obj.Time = t
587  objects += obj.Group
588  elif obj.Proxy.__class__.__name__ == "RobWorldProxy":
589  objects += obj.Group
590 

◆ exportAborted()

def Control.ControlPanel.exportAborted (   self)

Feedback method called when abort button was clicked.

The part of the dialog panel used for video exporting is closed.

Definition at line 901 of file Control.py.

901  def exportAborted(self):
902  # Close the export subform
903  self.closeExportSubform()
904 

◆ exportClicked()

def Control.ControlPanel.exportClicked (   self)

Feedback method called when export button was clicked.

Invalid buttons are disabled. An Export Path is checked for files. The files are checked for sequences. Sequences are shown with buttons to confirm or cancel the selection.

Definition at line 277 of file Control.py.

277  def exportClicked(self):
278  # Check that Export Path is valid
279  if not os.access(self.control_proxy.ExportPath, os.W_OK | os.R_OK):
280  # Show error if not
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.")
285  self.pauseClicked()
286  return
287 
288  # Disable everything
289  self.last_clicked = "export"
290  self.setInvalidButtons()
291 
292  # Try to load file names from an export folder
293  try:
294  files = os.listdir(self.control_proxy.ExportPath)
295  except FileNotFoundError as e:
296  QMessageBox.warning(None, 'Export Path error', str(e))
297  return
298 
299  # Find all recorded sequences between the files
300  sequences = self.findSequences(files)
301  if sequences != {}:
302  # Show them in an export menu
303  self.showSequences(sequences)
304  else:
305  # Show error if none found
306  QMessageBox.warning(None, 'Export error',
307  "No sequences to export.")
308  self.last_clicked = "pause"
309  self.setInvalidButtons()
310 

◆ exportConfirmed()

def Control.ControlPanel.exportConfirmed (   self)

Feedback method called when confirm button was clicked.

Buttons are disabled, framerate is loaded from the first image chunks, selected sequence name is used to create an image name template and a video name which can be used in a FFMPEG command. Such a command is executed to convert the video, if FFMPEG is installed. Otherwise warnings are shown.

Definition at line 841 of file Control.py.

841  def exportConfirmed(self):
842  # Disable export and confirm buttons
843  self.btn_confirm.setEnabled(False)
844  self.btn_abort.setEnabled(False)
845 
846  # Prepare arguments for ffmpeg conversion
847  selected_seq = \
848  self.trv_sequences.selectionModel().selectedRows()[0].data()
849  # Load framerate
850  image_name = selected_seq + "-" + (NAME_NUMBER_FORMAT % 0) + ".png"
851  image_path = path.join(self.control_proxy.ExportPath, image_name)
852  # load fps from the first image
853  fps = self.readFramerateChunk(image_path)
854  if fps == -1.0:
855  fps = 1 / self.control_proxy.StepTime
856  QMessageBox.warning(
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) = "
861  + str(fps) + ".")
862 
863  image_name = '"' + path.normpath(
864  path.join(self.control_proxy.ExportPath, selected_seq + "-"
865  + NAME_NUMBER_FORMAT + ".png")) + '"'
866  video_name = '"' + path.normpath(
867  path.join(self.control_proxy.ExportPath,
868  selected_seq + ".mp4")) + '"'
869 
870  # Prepare an ffmpeg command
871  export_command = 'ffmpeg -r ' + str(fps) + ' -i ' + image_name \
872  + ' -c:v libx264 -pix_fmt yuv420p ' + video_name
873 
874  # Try to run the command
875  try:
876  return_val = subprocess.call(export_command)
877  except OSError as e:
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")
882  else:
883  QMessageBox.warning(None, 'Something failed', str(e))
884  if return_val == 0:
885  QMessageBox.information(None, 'Export successful!',
886  "FFMPEG successfully converted image "
887  + "sequence into a video.")
888  else:
889  QMessageBox.warning(None, 'FFMPEG unsuccessfull',
890  "FFMPEG failed to convert sequence into "
891  + "a video")
892 
893  # Close the export subform
894  self.closeExportSubform()
895 

◆ findSequences()

def Control.ControlPanel.findSequences (   self,
  files 
)

Method to find sequences between files.

Files are scanned for sequences, the valid sequences are recognized and number of frames is counted.

Parameters
filesA list of string file names.
Returns
A dict with sequence names and numbers of frames.

Definition at line 677 of file Control.py.

677  def findSequences(self, files):
678  # Check there are any files
679  if len(files) == 0:
680  return {}
681 
682  # Go through the files
683  sequences = {}
684  for f in files:
685 
686  # Check they fit the name pattern
687  img_name = re.search(r"(seq\d+)-(\d+)(?=\.png)", f)
688  if img_name is not None:
689 
690  # Add new sequences
691  if img_name.group(1) not in list(sequences.keys()):
692 
693  # Add sequence if it's starting with 0
694  if int(img_name.group(2)) == 0:
695  sequences[img_name.group(1)] = 1
696  last_frame = int(img_name.group(2))
697 
698  # Compute number of successive frames
699  elif int(img_name.group(2)) == (last_frame + 1):
700  sequences[img_name.group(1)] += 1
701  last_frame += 1
702 
703  # Remove sequence if a frame is missing
704  else:
705  sequences.pop(img_name.group(1))
706 
707  # Leave sequences longer than 1 frame
708  sequences = {key: val for key, val in sequences.items() if val > 1}
709  return sequences
710 

◆ getStandardButtons()

def Control.ControlPanel.getStandardButtons (   self,
args 
)

Method to set just one button (close) to close the dialog.

*args: A tuple of unused arguments from Qt.

Definition at line 381 of file Control.py.

381  def getStandardButtons(self, *args):
382  return QDialogButtonBox.Close
383 

◆ installPyPNGNotice()

def Control.ControlPanel.installPyPNGNotice (   self)

Method telling user that pyPNG library ought to be installed into FreeCAD.

The pyPNG library is not part of FreeCAD and so we need to add it using pip. This method tells user to do so.

Definition at line 930 of file Control.py.

930  def installPyPNGNotice(self):
931  QMessageBox.information(
932  None, "Install PyPNG", "PyPNG is missing from your FreeCAD\n"
933  + "Please follow these instructions to install it:\n\n"
934  + "Windows:\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')
945 
946 # Alternative way to install it directly from FreeCAD
947 # import pip
948 # if hasattr(pip, "main"):
949 # FreeCAD.Console.PrintLog("Installing pyPNG.\n")
950 # if pip.main(["install", "pyPNG"]) != 0:
951 # FreeCAD.Console.PrintError("pyPNG installation failed.\n")
952 # FreeCAD.Console.PrintLog("Installation successful.\n")
953 # else:
954 # import pip._internal
955 # if hasattr(pip._internal, "main"):
956 # if pip._internal.main(["install", "pyPNG"]) != 0:
957 # FreeCAD.Console.PrintError("pyPNG installation failed.\n")
958 # FreeCAD.Console.PrintLog("Installation successful.\n")
959 # else:
960 # FreeCAD.Console.PrintLog(
961 # "Unable to import and install pyPNG.\n")
962 

◆ isAllowedAlterDocument()

def Control.ControlPanel.isAllowedAlterDocument (   self)

Method to tell FreeCAD if dialog is allowed to alter a document.

Returns
True this dialog does change a document.

Definition at line 408 of file Control.py.

408  def isAllowedAlterDocument(self):
409  return True
410 

◆ isAllowedAlterSelection()

def Control.ControlPanel.isAllowedAlterSelection (   self)

Method to tell FreeCAD if dialog is allowed to alter a selection.

Returns
False this dialog does not change a selection.

Definition at line 390 of file Control.py.

390  def isAllowedAlterSelection(self):
391  return False
392 

◆ isAllowedAlterView()

def Control.ControlPanel.isAllowedAlterView (   self)

Method to tell FreeCAD if dialog is allowed to alter a view.

Returns
True this dialog does change a view.

Definition at line 399 of file Control.py.

399  def isAllowedAlterView(self):
400  return True
401 

◆ pauseClicked()

def Control.ControlPanel.pauseClicked (   self)

Feedback method called when pause button was clicked.

Invalid buttons are disabled in this method and that's it.

Definition at line 189 of file Control.py.

189  def pauseClicked(self):
190  # Enable everything except for the pause button
191  self.last_clicked = "pause"
192  self.setInvalidButtons()
193 

◆ play()

def Control.ControlPanel.play (   self,
  t 
)

Method to show an animation frame at an animation time t during playing.

Current clock time is loaded. If the pause button was clicked, an animation is stopped. Otherwise the animation time t is distributed to appropriate children. If the animation time t exceeded Stop Time, the animation is stopped. Lastly next frame time is computed as well as pause time (to stick with real time if computation did not exceeded Step Time). Finally the timer is set to show the next animation frame after precomputed pause.

Parameters
tAn animation time to generate an animation frame at.

Definition at line 425 of file Control.py.

425  def play(self, t):
426  # Load current time
427  time_ = time.clock()
428 
429  # Check pause button was not pressed
430  if self.last_clicked == "pause":
431  return
432 
433  # Disribute the animation time to trajectories so that they change
434  # positions of all animated objects
435  self.distributeTime(t)
436  self.updateCollisions()
437  self.showChanges()
438 
439  # Display current progress on the seek slider
440  self.form.sld_seek.setValue(
441  numpy.round(100*(t - self.control_proxy.StartTime)
442  / (self.control_proxy.StopTime
443  - self.control_proxy.StartTime)))
444 
445  # Stop the animation if the animation time reached a range boundary
446  if t >= self.control_proxy.StopTime:
447  self.last_clicked = "pause"
448  self.setInvalidButtons()
449  return
450 
451  # Compute an animation time for the next frame
452  next_t = min(t + self.control_proxy.StepTime,
453  self.control_proxy.StopTime)
454 
455  # Compute pause period so that animaiton time roughly corresponds to
456  # the real time
457  pause = round(1000*(self.control_proxy.StepTime + time_
458  - time.clock()))
459  pause = pause*(pause > 0)
460 
461  # Setup a timer to show next frame if animaiton wasn't paused
462  if self.last_clicked != "pause":
463  self.timer.singleShot(pause, lambda: self.play(next_t))
464 

◆ playClicked()

def Control.ControlPanel.playClicked (   self)

Feedback method called when play button was clicked.

Invalid buttons are disabled. Active View's animation is disabled (Necessary). Slider position is checked for invalid position (at the end) and if position is plausible, all collisions are reset, current time is extrapolated from the slider and an animation is played.

Definition at line 162 of file Control.py.

162  def playClicked(self):
163  # Disable everything except for the pause button
164  self.last_clicked = "play"
165  self.setInvalidButtons()
166  FreeCADGui.ActiveDocument.ActiveView.setAnimationEnabled(False)
167 
168  # Check that we are not already at the end of an animation range
169  if self.form.sld_seek.value() == self.form.sld_seek.maximum():
170  # Show error if we are
171  QMessageBox.warning(None, 'Error while playing',
172  "The animation is at the end.")
173  self.pauseClicked()
174  else:
175  # Reset collisions
176  self.resetCollisions()
177  # Load current time from the time slider and start playing
178  t = self.form.sld_seek.value() \
179  * (self.control_proxy.StopTime
180  - self.control_proxy.StartTime) / 100 \
181  + self.control_proxy.StartTime
182  self.play(t)
183 

◆ readFramerateChunk()

def Control.ControlPanel.readFramerateChunk (   self,
  image_path 
)

Method to read a framerate inserted as one of a PNG image's chunks.

This method tries to import pyPNG first. Then it tries to install it and import again. If either import is successful, all chunks currently in the PNG image at an image_path are extracted. The framerate chunk ought to be stored as the second chunk, right behind IHDR. If the chunk's code type matches, its value is returned.

Parameters
image_pathA str containing a path to an image with the framerate chunk.
Returns
A float signifying framerate, or -1.0 if something failed.

Definition at line 1014 of file Control.py.

1014  def readFramerateChunk(self, image_path):
1015  # import or install pyPNG
1016  try:
1017  import png
1018  except ModuleNotFoundError:
1019  self.installPyPNGNotice()
1020  return -1.0
1021  # Read chunks already present in a PNG image
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]
1026  else:
1027  FreeCAD.Console.PrintError("Unable to unpack a framerate.\n")
1028  return -1.0
1029 
1030 

◆ record()

def Control.ControlPanel.record (   self,
  t 
)

Method to show and save an animation frame at an animation time t.

Current clock time is loaded. If the pause button was clicked, an animation is stopped. Otherwise the animation time t is distributed to appropriate children. If the animation time t exceeded Stop Time, the animation is stopped. Lastly next frame time is computed. Finally the timer is set to show the next animation frame after precomputed pause.

Parameters
tAn animation time to generate an animation frame at.

Definition at line 532 of file Control.py.

532  def record(self, t):
533  # Check pause button was not pressed
534  if self.last_clicked == "pause":
535  return
536 
537  # Disribute the animation time to trajectories so that they change
538  # positions of all animated objects, save the image
539  self.distributeTime(t)
540  self.updateCollisions()
541 
542  # Show changes and save view
543  self.showChanges()
544  self.saveImage()
545 
546  # Display current progress on the seek slider
547  self.form.sld_seek.setValue(
548  numpy.round(100*(t - self.control_proxy.StartTime)
549  / (self.control_proxy.StopTime
550  - self.control_proxy.StartTime)))
551 
552  # Stop the animation if the animation time reached a range boundary
553  if t >= self.control_proxy.StopTime:
554  self.last_clicked = "pause"
555  self.setInvalidButtons()
556  return
557 
558  # Compute an animation time for the next frame
559  next_t = min(t + self.control_proxy.StepTime,
560  self.control_proxy.StopTime)
561 
562  # Setup a timer to show next frame if animaiton wasn't paused
563  if self.last_clicked != "pause":
564  self.timer.singleShot(0, lambda: self.record(next_t))
565 

◆ recordClicked()

def Control.ControlPanel.recordClicked (   self)

Feedback method called when record button was clicked.

Invalid buttons are disabled. A record prefix is generated. An Image number is set to 0. Active View's animation is disabled (Necessary). Slider position is checked for invalid position (at the end) and if position is plausible, all collisions are reset, current time is extrapolated from the slider and an animation is played/recorded.

Definition at line 233 of file Control.py.

233  def recordClicked(self):
234  # Disable everything except for the pause button
235  self.last_clicked = "record"
236  self.setInvalidButtons()
237 
238  # Create an unique prefix for the image files which will be made
239  self.record_prefix = "seq" + time.strftime("%Y%m%d%H%M%S") + "-"
240  # Reset image number for new image sequence
241  self.image_number = 0
242  FreeCADGui.ActiveDocument.ActiveView.setAnimationEnabled(False)
243 
244  # Check that we are not already at the end of an animation range
245  if self.form.sld_seek.value() == self.form.sld_seek.maximum():
246  # Show error if we are
247  QMessageBox.warning(None, 'Error while playing',
248  "The animation is at the end.")
249  self.pauseClicked()
250 
251  # Check that Export Path is valid
252  elif not os.access(self.control_proxy.ExportPath, os.W_OK | os.R_OK):
253  # Show error if not
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.")
258  self.pauseClicked()
259 
260  else:
261  # Reset collisions
262  self.resetCollisions()
263  # Load current time from the time slider and start recording
264  t = self.form.sld_seek.value() \
265  * (self.control_proxy.StopTime
266  - self.control_proxy.StartTime) / 100 \
267  + self.control_proxy.StartTime
268  self.record(t)
269 

◆ reject()

def Control.ControlPanel.reject (   self)

Feedback method called when Control panel is closing.

Animation is stopped. Controls properties are set to be editable. Dialog is closed.

Definition at line 360 of file Control.py.

360  def reject(self):
361  # Stop animaiton, if it's running by clicking pause button
362  self.pauseClicked()
363 
364  # Allow editing of Control properties again
365  for prop in self.control_proxy.PropertiesList:
366  self.control_proxy.setEditorMode(prop, 0)
367 
368  # Delete reference to this panel from the view provider as the panel
369  # will no longer exist
370  self.control_proxy.ViewObject.Proxy.panel = None
371 
372  # Close the dialog
373  FreeCADGui.Control.closeDialog()
374 

◆ resetCollisions()

def Control.ControlPanel.resetCollisions (   self)

Method to reset collisions from CollisionDetector children.

List of children is loaded. If a child is CollisionDetector, it's reset.

Definition at line 612 of file Control.py.

612  def resetCollisions(self):
613  # Load list of objects inside Control group
614  objects = self.control_proxy.Group
615 
616  # if they are CollisionDetectors, then check for collisions
617  while len(objects) > 0:
618  obj = objects.pop(0)
619  if obj.Proxy.__class__.__name__ == "CollisionDetectorProxy":
620  obj.Proxy.reset()
621 

◆ rewind()

def Control.ControlPanel.rewind (   self,
  t 
)

Method to show an animation frame at an animation time t during rewind.

Current clock time is loaded. If the pause button was clicked, an animation is stopped. Otherwise the animation time t is distributed to appropriate children. If the animation time t exceeded Stop Time, the animation is stopped. Lastly next frame time is computed as well as pause time (to stick with real time if computation did not exceeded Step Time). Finally the timer is set to show the next animation frame after precomputed pause.

Parameters
tAn animation time to generate an animation frame at.

Definition at line 479 of file Control.py.

479  def rewind(self, t):
480  # Load current time
481  time_ = time.clock()
482 
483  # Check pause button was not pressed
484  if self.last_clicked == "pause":
485  return
486 
487  # Disribute the animation time to trajectories so that they change
488  # positions of all animated objects
489  self.distributeTime(t)
490  self.updateCollisions()
491  self.showChanges()
492 
493  # Display current progress on the seek slider
494  self.form.sld_seek.setValue(
495  numpy.round(100*(t - self.control_proxy.StartTime)
496  / (self.control_proxy.StopTime
497  - self.control_proxy.StartTime)))
498 
499  # Stop the animation if the animation time reached a range boundary
500  if t <= self.control_proxy.StartTime:
501  self.last_clicked = "pause"
502  self.setInvalidButtons()
503  return
504 
505  # Compute an animation time for the next frame
506  next_t = max(t - self.control_proxy.StepTime,
507  self.control_proxy.StartTime)
508 
509  # Compute pause period so that animaiton time roughly corresponds to
510  # the real time
511  pause = round(1000*(self.control_proxy.StepTime + time_
512  - time.clock()))
513  pause = pause*(pause > 0)
514 
515  # Setup a timer to show next frame if animaiton wasn't paused
516  if self.last_clicked != "pause":
517  self.timer.singleShot(pause, lambda: self.rewind(next_t))
518 

◆ rewindClicked()

def Control.ControlPanel.rewindClicked (   self)

Feedback method called when rewind button was clicked.

Invalid buttons are disabled. Active View's animation is disabled (Necessary). Slider position is checked for invalid position (at the end) and if position is plausible, all collisions are reset, current time is extrapolated from the slider and an animation is played.

Definition at line 202 of file Control.py.

202  def rewindClicked(self):
203  # Disable everything except for the pause button
204  self.last_clicked = "rewind"
205  self.setInvalidButtons()
206  FreeCADGui.ActiveDocument.ActiveView.setAnimationEnabled(False)
207 
208  # Check that we are not already at the start of an animation range
209  if self.form.sld_seek.value() == self.form.sld_seek.minimum():
210  # Show error if we are
211  QMessageBox.warning(None, 'Error while rewinding',
212  "The animation is at the beginning.")
213  self.pauseClicked()
214  else:
215  # Reset collisions
216  self.resetCollisions()
217  # Load current time from the time slider and start rewinding
218  t = self.form.sld_seek.value() \
219  * (self.control_proxy.StopTime
220  - self.control_proxy.StartTime) / 100 \
221  + self.control_proxy.StartTime
222  self.rewind(t)
223 

◆ saveImage()

def Control.ControlPanel.saveImage (   self)

Method to save current view as a PNG image.

An image name is pieced together from record prefix and image number. Then an image path is constructed. Animation is disabled(obligatory) and current view is saved as an image. Afterwards, if saving the first image(image number 0), a chunk with a framerate corresponding to a step size is added. Finally the image number is incremented.

Definition at line 641 of file Control.py.

641  def saveImage(self):
642  # Prepare complete path to an image
643  name = self.record_prefix + (NAME_NUMBER_FORMAT % self.image_number) \
644  + ".png"
645  image_path = path.join(self.control_proxy.ExportPath, name)
646 
647  # Export image and increase image number
648  FreeCADGui.ActiveDocument.ActiveView.setAnimationEnabled(False)
649  FreeCADGui.ActiveDocument.ActiveView.saveImage(
650  image_path,
651  self.control_proxy.VideoWidth, self.control_proxy.VideoHeight)
652 
653  # Write a framerate chunk into the first image
654  if self.image_number == 0:
655  if not self.writeFramerateChunk(1 / self.control_proxy.StepTime,
656  image_path):
657  QMessageBox.warning(
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.")
663  self.image_number += 1
664 

◆ setInvalidButtons()

def Control.ControlPanel.setInvalidButtons (   self)

Method to enable/disable buttons according to a last clicked button.

If pause button was pressed, all others buttons are disabled. If any other button was pressed, only pause button is left enabled.

Definition at line 337 of file Control.py.

337  def setInvalidButtons(self):
338  # Disable invalid buttons with respect to the last clicked button
339  self.form.btn_play.setEnabled(self.last_clicked == "pause" and
340  self.last_clicked != "export")
341  self.form.btn_pause.setEnabled(self.last_clicked != "pause" and
342  self.last_clicked != "export")
343  self.form.btn_rewind.setEnabled(self.last_clicked == "pause" and
344  self.last_clicked != "export")
345  self.form.btn_record.setEnabled(self.last_clicked == "pause" and
346  self.last_clicked != "export")
347  self.form.btn_export.setEnabled(self.last_clicked == "pause" and
348  self.last_clicked != "export")
349  self.form.lbl_seek.setEnabled(self.last_clicked == "pause" and
350  self.last_clicked != "export")
351  self.form.sld_seek.setEnabled(self.last_clicked == "pause" and
352  self.last_clicked != "export")
353 

◆ showChanges()

def Control.ControlPanel.showChanges (   self)

Method to show changes made to objects, collisions.

This method is necessary to call after distributeTime, updateCollisions and resetCollisions.

Definition at line 628 of file Control.py.

628  def showChanges(self):
629  FreeCAD.ActiveDocument.recompute()
630  FreeCADGui.updateGui()
631 

◆ showSequences()

def Control.ControlPanel.showSequences (   self,
  sequences 
)

Method to show sequences to export on a dialog panel.

Sequences and frame numbers are shown in a QTreeView, and buttons ‘'Confirm’ and'Abort'` are attached under it. All of this is put under the Export button on the dialog panel.

Parameters
sequencesA dict with sequence names and numbers of frames.

Definition at line 721 of file Control.py.

721  def showSequences(self, sequences):
722  # Add names to columns
723  NAME, N_FRAMES = range(2)
724 
725  # Create a tree view and set it up
726  self.trv_sequences = QTreeView()
727  self.trv_sequences.setRootIsDecorated(False)
728  self.trv_sequences.setAlternatingRowColors(True)
729  self.trv_sequences.setToolTip("Select a sequence to export.")
730  self.trv_sequences.setSizeAdjustPolicy(
731  self.trv_sequences.AdjustToContents)
732  self.trv_sequences.setSizePolicy(
733  self.trv_sequences.sizePolicy().Ignored,
734  self.trv_sequences.sizePolicy().Minimum)
735  self.trv_sequences.header().setResizeMode(
736  self.trv_sequences.header().Fixed)
737  self.trv_sequences.header().setDefaultSectionSize(120)
738  self.trv_sequences.setSelectionMode(self.trv_sequences.SingleSelection)
739 
740  # Prepare a table
741  model = QStandardItemModel(0, 2, self.trv_sequences)
742 
743  # Prepare a header
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)
749 
750  # Add data to the table
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))
760 
761  # Add the table to the tree view
762  self.trv_sequences.setModel(model)
763 
764  # Add the tree view to the panel under the EXPORT button
765  self.form.lyt_main.insertWidget(5, self.trv_sequences)
766 
767  # Make column with the numbers of frames smaller
768  self.trv_sequences.setColumnWidth(1, 80)
769  # Select the first item
770  self.trv_sequences.setCurrentIndex(model.index(0, 0))
771 
772  # Add horizontal layout under the tree view
773  self.lyt_export = QHBoxLayout()
774  self.form.lyt_main.insertLayout(6, self.lyt_export)
775 
776  # Add buttons for confirmation of a selected sequence and
777  # export abortion
778  self.btn_confirm = QPushButton("Confirm")
779  self.btn_confirm.setStyleSheet(
780  """
781  QPushButton {
782  background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
783  stop: 0 #0B0, stop: 1.0 #0D0);
784  font-weight: bold;
785  }
786  QPushButton:hover {border-color: #0D0;}
787  QPushButton:focus {
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;
791  }
792  QPushButton:pressed {
793  background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
794  stop: 0 #0F0, stop: 1.0 #0C0);
795  }""")
796  self.btn_confirm.clicked.connect(self.exportConfirmed)
797  self.btn_abort = QPushButton("Abort")
798  self.btn_abort.setStyleSheet(
799  """
800  QPushButton {
801  background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
802  stop: 0 #B00, stop: 1.0 #D00);
803  font-weight: bold;
804  }
805  QPushButton:hover {border-color: #D00;}
806  QPushButton:focus {
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;
810  }
811  QPushButton:pressed {
812  background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
813  stop: 0 #F00, stop: 1.0 #C00);
814  }""")
815  self.btn_abort.clicked.connect(self.exportAborted)
816  self.lyt_export.addWidget(self.btn_confirm)
817  self.lyt_export.addWidget(self.btn_abort)
818 
819  # Create a function to disable deselection
820  def mySelectionChanged(selected, deselected):
821  if selected.isEmpty() and not deselected.isEmpty():
822  self.trv_sequences.selectionModel().select(
823  deselected.first().indexes()[0],
824  self.trv_sequences.selectionModel().Select
825  | self.trv_sequences.selectionModel().Rows)
826 
827  # Connect the function as a slot for signal emitted when selection is
828  # changed
829  self.trv_sequences.selectionModel().selectionChanged.connect(
830  mySelectionChanged)
831 

◆ sliderChanged()

def Control.ControlPanel.sliderChanged (   self)

Feedback method called when slider position is changed.

If slider is enabled (not used to show animation time) and slider position is changed, time is extrapolated from slider position and animation in that time is shown.

Definition at line 318 of file Control.py.

318  def sliderChanged(self):
319  # Check if the slider is enabled i.e. the change is an user input,
320  # not a visualization of animation progress
321  if self.form.sld_seek.isEnabled():
322  # Load current time from the time slider and show it.
323  t = self.form.sld_seek.value() \
324  * (self.control_proxy.StopTime
325  - self.control_proxy.StartTime) / 100 \
326  + self.control_proxy.StartTime
327  self.distributeTime(t)
328  self.updateCollisions()
329  self.showChanges()
330 

◆ updateCollisions()

def Control.ControlPanel.updateCollisions (   self)

Method to update collisions from CollisionDetector children.

List of children is loaded. If a child is CollisionDetector, it's touched so that it's recomputed.

Definition at line 597 of file Control.py.

597  def updateCollisions(self):
598  # Load list of objects inside Control group
599  objects = self.control_proxy.Group
600 
601  # if they are CollisionDetectors, then check for collisions
602  while len(objects) > 0:
603  obj = objects.pop(0)
604  if obj.Proxy.__class__.__name__ == "CollisionDetectorProxy":
605  obj.touch()
606 

◆ writeFramerateChunk()

def Control.ControlPanel.writeFramerateChunk (   self,
  framerate,
  image_path 
)

Method to write a framerate into a PNG image as one of its chunks.

This method tries to import pyPNG first. Then it tries to install it and import again. If either import is successful, all chunks currently in the PNG image at an image_path are extracted. The framerate chunk is added as the second chunk, right behind IHDR. Finally the image is rewritten with new list of chunks.

Parameters
framerateA float specifying the framerate to be written into the image.
image_pathA str containing a path to an image about to be augmented.

Definition at line 976 of file Control.py.

976  def writeFramerateChunk(self, framerate, image_path):
977  # import or install pyPNG
978  try:
979  import png
980  except ModuleNotFoundError:
981  self.installPyPNGNotice()
982  return False
983  except Exception as e:
984  FreeCAD.Console.PrintError(
985  "Unexpected error occurred while importing pyPNG - " + str(e))
986 
987  # Read chunks already present in a PNG image
988  reader = png.Reader(filename=image_path)
989  chunks = list(reader.chunks())
990  # Insert custom framerate chunk
991  chunks.insert(1, (FPS_CHUNK_CODE, struct.pack("f", framerate)))
992 
993  # Write it into the image
994  with open(image_path, 'wb') as image_file:
995  png.write_chunks(image_file, chunks)
996 
997  return True
998 

Member Data Documentation

◆ btn_abort

Control.ControlPanel.btn_abort

A QPushButton to abort exporting a sequence.

Definition at line 797 of file Control.py.

◆ btn_confirm

Control.ControlPanel.btn_confirm

A QPushButton to confirm sequence to export.

Definition at line 778 of file Control.py.

◆ control_proxy

Control.ControlPanel.control_proxy

A proxy to an associated Control class.

Definition at line 129 of file Control.py.

◆ form

Control.ControlPanel.form

A QDialog instance show in the TaskView.

Definition at line 136 of file Control.py.

◆ image_number

Control.ControlPanel.image_number

An int number of a next recorded image.

Definition at line 241 of file Control.py.

◆ last_clicked

Control.ControlPanel.last_clicked

A str showing which button was pressed last.

Definition at line 151 of file Control.py.

◆ lyt_export

Control.ControlPanel.lyt_export

A QHBoxLayout with a confirm and abort buttons.

Definition at line 773 of file Control.py.

◆ record_prefix

Control.ControlPanel.record_prefix

A str prefix for an image file name.

Definition at line 239 of file Control.py.

◆ timer

Control.ControlPanel.timer

A QTimer for timing animations.

Definition at line 147 of file Control.py.

◆ trv_sequences

Control.ControlPanel.trv_sequences

A QTreeView showing list of recorded sequences.

Definition at line 726 of file Control.py.


The documentation for this class was generated from the following file: