| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import re |
| | import os |
| | import time |
| | import FreeCAD as App |
| |
|
| | from pivy import coin |
| | from Part import LineSegment, Compound |
| |
|
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| |
|
| | if App.GuiUp: |
| | import FreeCADGui as Gui |
| | from PySide import QtCore, QtGui, QtWidgets |
| | from PySide.QtWidgets import ( |
| | QPushButton, |
| | QMenu, |
| | QDialog, |
| | QComboBox, |
| | QLineEdit, |
| | QGridLayout, |
| | QLabel, |
| | QDialogButtonBox, |
| | ) |
| | from PySide.QtCore import Qt, QPoint |
| | from PySide.QtGui import QCursor, QIcon, QGuiApplication |
| |
|
| | import UtilsAssembly |
| | import Preferences |
| |
|
| | translate = App.Qt.translate |
| |
|
| | __title__ = "Assembly Command Create Simulation" |
| | __author__ = "Ondsel" |
| | __url__ = "https://www.freecad.org" |
| |
|
| |
|
| | class CommandCreateSimulation: |
| | def __init__(self): |
| | pass |
| |
|
| | def GetResources(self): |
| | return { |
| | "Pixmap": "Assembly_CreateSimulation", |
| | "MenuText": QT_TRANSLATE_NOOP("Assembly_CreateSimulation", "Simulation"), |
| | "Accel": "V", |
| | "ToolTip": QT_TRANSLATE_NOOP( |
| | "Assembly_CreateSimulation", |
| | "Creates a new simulation of the current assembly", |
| | ), |
| | "CmdType": "ForEdit", |
| | } |
| |
|
| | def IsActive(self): |
| | if not UtilsAssembly.isAssemblyCommandActive(): |
| | return False |
| |
|
| | assembly = UtilsAssembly.activeAssembly() |
| | joint_types = ["Revolute", "Slider", "Cylindrical"] |
| | joints = UtilsAssembly.getJointsOfType(assembly, joint_types) |
| | return len(joints) > 0 |
| |
|
| | def Activated(self): |
| | assembly = UtilsAssembly.activeAssembly() |
| | if not assembly: |
| | return |
| |
|
| | self.panel = TaskAssemblyCreateSimulation() |
| | dialog = Gui.Control.showDialog(self.panel) |
| | if dialog is not None: |
| | dialog.setAutoCloseOnDeletedDocument(True) |
| | dialog.setDocumentName(App.ActiveDocument.Name) |
| |
|
| |
|
| | |
| | class Simulation: |
| | def __init__(self, feaPy): |
| | feaPy.Proxy = self |
| | feaPy.addExtension("App::GroupExtensionPython") |
| |
|
| | if not hasattr(feaPy, "aTimeStart"): |
| | feaPy.addProperty( |
| | "App::PropertyTime", |
| | "aTimeStart", |
| | "Simulation", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Simulation start time.", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(feaPy, "bTimeEnd"): |
| | feaPy.addProperty( |
| | "App::PropertyTime", |
| | "bTimeEnd", |
| | "Simulation", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Simulation end time.", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(feaPy, "cTimeStepOutput"): |
| | feaPy.addProperty( |
| | "App::PropertyTime", |
| | "cTimeStepOutput", |
| | "Simulation", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Simulation time step for output.", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(feaPy, "fGlobalErrorTolerance"): |
| | feaPy.addProperty( |
| | "App::PropertyFloat", |
| | "fGlobalErrorTolerance", |
| | "Simulation", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Integration global error tolerance.", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(feaPy, "jFramesPerSecond"): |
| | feaPy.addProperty( |
| | "App::PropertyInteger", |
| | "jFramesPerSecond", |
| | "Simulation", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "Frames Per Second.", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | feaPy.aTimeStart = 0.0 |
| | feaPy.bTimeEnd = 1.0 |
| | feaPy.cTimeStepOutput = 1.0e-2 |
| | feaPy.fGlobalErrorTolerance = 1.0e-6 |
| | feaPy.jFramesPerSecond = 30 |
| |
|
| | self.motionsChangedCallback = None |
| |
|
| | def dumps(self): |
| | return None |
| |
|
| | def loads(self, state): |
| | return None |
| |
|
| | def onChanged(self, feaPy, prop): |
| | if prop == "Group" and hasattr(self, "motionsChangedCallback"): |
| | if self.motionsChangedCallback is not None: |
| | self.motionsChangedCallback() |
| |
|
| | def setMotionsChangedCallback(self, callback): |
| | self.motionsChangedCallback = callback |
| |
|
| | def execute(self, feaPy): |
| | """Do something when doing a recomputation, this method is mandatory""" |
| | pass |
| |
|
| | def getAssembly(self, feaPy): |
| | assert feaPy.isDerivedFrom("App::FeaturePython"), "Type error" |
| | for obj in feaPy.InList: |
| | if obj.isDerivedFrom("Assembly::AssemblyObject"): |
| | return obj |
| | return None |
| |
|
| |
|
| | class ViewProviderSimulation: |
| | def __init__(self, vpDoc): |
| | vpDoc.Proxy = self |
| | self.Object = vpDoc.Object |
| | self.setProperties(vpDoc) |
| |
|
| | def setProperties(self, vpDoc): |
| | if not hasattr(vpDoc, "Decimals"): |
| | vpDoc.addProperty( |
| | "App::PropertyInteger", |
| | "Decimals", |
| | "Space", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", "The number of decimals to use for calculated texts" |
| | ), |
| | locked=True, |
| | ) |
| | vpDoc.Decimals = 9 |
| |
|
| | def attach(self, vpDoc): |
| | """Setup the scene sub-graph of the view provider, this method is mandatory""" |
| | self.app_obj = vpDoc.Object |
| |
|
| | self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance() |
| |
|
| | vpDoc.addDisplayMode(self.display_mode, "Wireframe") |
| |
|
| | def updateData(self, feaPy, prop): |
| | """If a property of the handled feature has changed we have the chance to handle this here""" |
| | pass |
| |
|
| | def getDisplayModes(self, vpDoc): |
| | """Return a list of display modes.""" |
| | return ["Wireframe"] |
| |
|
| | def getDefaultDisplayMode(self): |
| | """Return the name of the default display mode. It must be defined in getDisplayModes.""" |
| | return "Wireframe" |
| |
|
| | def onChanged(self, vpDoc, prop): |
| | """Here we can do something when a single property got changed""" |
| | pass |
| |
|
| | def getIcon(self): |
| | return ":/icons/Assembly_CreateSimulation.svg" |
| |
|
| | def dumps(self): |
| | """When saving the document this object gets stored using Python's json module.\ |
| | Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\ |
| | to return a tuple of all serializable objects or None.""" |
| | return None |
| |
|
| | def loads(self, state): |
| | """When restoring the serialized object from document we have the chance to set some internals here.\ |
| | Since no data were serialized nothing needs to be done here.""" |
| | return None |
| |
|
| | def claimChildren(self): |
| | return self.app_obj.Group |
| |
|
| | def doubleClicked(self, vpDoc): |
| | task = Gui.Control.activeTaskDialog() |
| | if task: |
| | task.reject() |
| |
|
| | assembly = vpDoc.Object.Proxy.getAssembly(vpDoc.Object) |
| |
|
| | if assembly is None: |
| | return False |
| |
|
| | if UtilsAssembly.activeAssembly() != assembly: |
| | Gui.ActiveDocument.setEdit(assembly) |
| |
|
| | panel = TaskAssemblyCreateSimulation(vpDoc.Object) |
| | dialog = Gui.Control.showDialog(panel) |
| | if dialog is not None: |
| | dialog.setAutoCloseOnDeletedDocument(True) |
| | dialog.setDocumentName(App.ActiveDocument.Name) |
| |
|
| | return True |
| |
|
| | def onDelete(self, vobj, subelements): |
| | for obj in self.claimChildren(): |
| | obj.Document.removeObject(obj.Name) |
| | return True |
| |
|
| |
|
| | |
| | MotionTypes = [ |
| | "Angular", |
| | "Linear", |
| | ] |
| |
|
| |
|
| | class Motion: |
| | def __init__(self, feaPy, motionType=MotionTypes[0], joint=None, formula=""): |
| | feaPy.Proxy = self |
| |
|
| | self.createProperties(feaPy) |
| |
|
| | feaPy.MotionType = MotionTypes |
| | feaPy.MotionType = motionType |
| | feaPy.Joint = joint |
| | feaPy.Formula = formula |
| |
|
| | def onDocumentRestored(self, feaPy): |
| | self.createProperties(feaPy) |
| |
|
| | def createProperties(self, feaPy): |
| | if not hasattr(feaPy, "Joint"): |
| | feaPy.addProperty( |
| | "App::PropertyXLinkSubHidden", |
| | "Joint", |
| | "Motion", |
| | QT_TRANSLATE_NOOP("App::Property", "The joint that is moved by the motion"), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(feaPy, "Formula"): |
| | feaPy.addProperty( |
| | "App::PropertyString", |
| | "Formula", |
| | "Motion", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "This is the formula of the motion. For example '1.0*time'.", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | if not hasattr(feaPy, "MotionType"): |
| | feaPy.addProperty( |
| | "App::PropertyEnumeration", |
| | "MotionType", |
| | "Motion", |
| | QT_TRANSLATE_NOOP("App::Property", "The type of the motion"), |
| | locked=True, |
| | ) |
| |
|
| | def dumps(self): |
| | return None |
| |
|
| | def loads(self, state): |
| | return None |
| |
|
| | def onChanged(self, feaPy, prop): |
| | pass |
| |
|
| | def execute(self, feaPy): |
| | """Do something when doing a recomputation, this method is mandatory""" |
| | pass |
| |
|
| | def getSimulation(self, feaPy): |
| | for obj in feaPy.InList: |
| | if hasattr(obj, "Proxy"): |
| | if hasattr(obj.Proxy, "setMotionsChangedCallback"): |
| | return obj |
| | return None |
| |
|
| | def getAssembly(self, feaPy): |
| | simulation = self.getSimulation(feaPy) |
| | if simulation is not None: |
| | return simulation.Proxy.getAssembly(simulation) |
| | return None |
| |
|
| |
|
| | class ViewProviderMotion: |
| | def __init__(self, vp): |
| | vp.Proxy = self |
| | self.updateLabel() |
| |
|
| | def attach(self, vpDoc): |
| | """Setup the scene sub-graph of the view provider, this method is mandatory""" |
| | self.app_obj = vpDoc.Object |
| |
|
| | self.display_mode = coin.SoType.fromName("SoFCSelection").createInstance() |
| |
|
| | vpDoc.addDisplayMode(self.display_mode, "Wireframe") |
| |
|
| | def updateData(self, feaPy, prop): |
| | """If a property of the handled feature has changed we have the chance to handle this here""" |
| | pass |
| |
|
| | def getDisplayModes(self, vpDoc): |
| | """Return a list of display modes.""" |
| | return ["Wireframe"] |
| |
|
| | def getDefaultDisplayMode(self): |
| | """Return the name of the default display mode. It must be defined in getDisplayModes.""" |
| | return "Wireframe" |
| |
|
| | def onChanged(self, vpDoc, prop): |
| | """Here we can do something when a single property got changed""" |
| | |
| | pass |
| |
|
| | def getIcon(self): |
| | if self.app_obj.MotionType == "Angular": |
| | return ":/icons/button_rotate.svg" |
| |
|
| | return ":/icons/button_right.svg" |
| |
|
| | def dumps(self): |
| | """When saving the document this object gets stored using Python's json module.\ |
| | Since we have some un-serializable parts here -- the Coin stuff -- we must define this method\ |
| | to return a tuple of all serializable objects or None.""" |
| | return None |
| |
|
| | def loads(self, state): |
| | """When restoring the serialized object from document we have the chance to set some internals here.\ |
| | Since no data were serialized nothing needs to be done here.""" |
| | return None |
| |
|
| | def doubleClicked(self, vpDoc): |
| | self.openEditDialog() |
| |
|
| | def openEditDialog(self): |
| | assembly = self.getAssembly() |
| |
|
| | if assembly is None: |
| | return False |
| |
|
| | joint = None |
| | if self.app_obj.Joint is not None: |
| | joint = self.app_obj.Joint[0] |
| |
|
| | dialog = MotionEditDialog(assembly, self.app_obj.MotionType, joint, self.app_obj.Formula) |
| | if dialog.exec_(): |
| | self.app_obj.MotionType = dialog.motionType |
| | self.app_obj.Joint = dialog.joint |
| | self.app_obj.Formula = dialog.formula |
| |
|
| | self.updateLabel() |
| |
|
| | def updateLabel(self): |
| | if self.app_obj.Joint is None: |
| | return |
| |
|
| | typeStr = "Linear" if self.app_obj.MotionType == "Linear" else "Angular" |
| |
|
| | self.app_obj.Label = "{label} ({type_})".format( |
| | label=self.app_obj.Joint[0].Label, type_=translate("Assembly", typeStr) |
| | ) |
| |
|
| | def getAssembly(self): |
| | assembly = self.app_obj.Proxy.getAssembly(self.app_obj) |
| |
|
| | if assembly is None: |
| | return None |
| |
|
| | if UtilsAssembly.activeAssembly() != assembly: |
| | Gui.ActiveDocument.setEdit(assembly) |
| |
|
| | return assembly |
| |
|
| |
|
| | class MotionEditDialog: |
| | def __init__(self, assembly, motionType=MotionTypes[0], joint=None, formula="5*time"): |
| | self.assembly = assembly |
| | self.motionType = motionType |
| | self.joint = joint |
| | self.formula = formula |
| |
|
| | |
| | self.dialog = QDialog() |
| | self.dialog.setWindowFlags(Qt.Popup) |
| | self.initialPos = QCursor.pos() |
| | self.dialog.setMinimumSize(500, 200) |
| |
|
| | |
| | self.joint_combo = QComboBox(self.dialog) |
| | self.setup_joint_combo() |
| |
|
| | |
| | self.motion_type_combo = QComboBox(self.dialog) |
| | self.setup_motiontype_combo() |
| |
|
| | def on_motion_type_changed(text): |
| | self.motionType = text |
| |
|
| | self.motion_type_combo.currentTextChanged.connect(on_motion_type_changed) |
| |
|
| | def on_joint_changed(index): |
| | self.joint = self.joint_combo.itemData(index) |
| | self.setup_motiontype_combo() |
| |
|
| | self.joint_combo.currentIndexChanged.connect(on_joint_changed) |
| |
|
| | |
| | formula_edit = QLineEdit(self.dialog) |
| | formula_edit.setText(self.formula) |
| | formula_edit.setPlaceholderText(translate("Assembly", "Enter your formula...")) |
| |
|
| | |
| | def on_formula_changed(text): |
| | self.formula = text |
| |
|
| | formula_edit.textChanged.connect(on_formula_changed) |
| |
|
| | self.setupHelpSection() |
| |
|
| | |
| | button_box = QDialogButtonBox( |
| | QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self.dialog |
| | ) |
| | button_box.accepted.connect(self.dialog.accept) |
| | button_box.rejected.connect(self.dialog.reject) |
| |
|
| | |
| | layout = QGridLayout(self.dialog) |
| |
|
| | |
| | layout.addWidget(QLabel("Joint"), 0, 0) |
| | layout.addWidget(self.joint_combo, 0, 1) |
| |
|
| | layout.addWidget(QLabel("Motion Type"), 1, 0) |
| | layout.addWidget(self.motion_type_combo, 1, 1) |
| |
|
| | layout.addWidget(QLabel("Formula"), 2, 0) |
| | layout.addWidget(formula_edit, 2, 1) |
| |
|
| | |
| | layout.addWidget(self.help_label0, 3, 0, 1, 2) |
| | layout.addWidget(self.help_label1, 4, 0, 1, 2) |
| | layout.addWidget(self.help_label2, 5, 0, 1, 2) |
| | layout.addWidget(self.help_label3, 6, 0, 1, 2) |
| | layout.addWidget(self.help_label4, 7, 0, 1, 2) |
| | layout.addWidget(self.help_label5, 8, 0, 1, 2) |
| | layout.addWidget(self.help_label6, 9, 0, 1, 2) |
| | layout.addWidget(self.help_label7, 10, 0, 1, 2) |
| | |
| | layout.addWidget(self.help_button, 11, 0) |
| |
|
| | layout.addWidget(button_box, 11, 1) |
| |
|
| | self.positionDialog() |
| |
|
| | def setupHelpSection(self): |
| |
|
| | |
| | self.help_label0 = QLabel( |
| | translate( |
| | "Assembly", |
| | "In capital are variables that you need to replace with actual values. More details about each example in its tooltip.", |
| | ), |
| | self.dialog, |
| | ) |
| | self.help_label1 = QLabel(translate("Assembly", " - Linear: C + VEL*time"), self.dialog) |
| | self.help_label2 = QLabel( |
| | translate("Assembly", " - Quadratic: C + VEL*time + ACC*time^2"), self.dialog |
| | ) |
| | self.help_label3 = QLabel( |
| | translate("Assembly", " - Harmonic: C + AMP*sin(VEL*time - PHASE)"), self.dialog |
| | ) |
| | self.help_label4 = QLabel( |
| | translate("Assembly", " - Exponential: C*exp(time/TIMEC)"), self.dialog |
| | ) |
| | self.help_label5 = QLabel( |
| | translate( |
| | "Assembly", |
| | " - Smooth Step: L1 + (L2 - L1)*((1/2) + (1/pi)*arctan(SLOPE*(time - T0)))", |
| | ), |
| | self.dialog, |
| | ) |
| | self.help_label6 = QLabel( |
| | translate( |
| | "Assembly", |
| | " - Smooth Square Impulse: (H/pi)*(arctan(SLOPE*(time - T1)) - arctan(SLOPE*(time - T2)))", |
| | ), |
| | self.dialog, |
| | ) |
| | self.help_label7 = QLabel( |
| | translate( |
| | "Assembly", |
| | " - Smooth Ramp Top Impulse: ((1/pi)*(arctan(1000*(time - T1)) - arctan(1000*(time - T2))))*(((H2 - H1)/(T2 - T1))*(time - T1) + H1)", |
| | ), |
| | self.dialog, |
| | ) |
| |
|
| | self.help_label1.setToolTip( |
| | translate( |
| | "Assembly", |
| | """C is a constant offset. |
| | VEL is a velocity or slope or gradient of the straight line.""", |
| | ) |
| | ) |
| | self.help_label2.setToolTip( |
| | translate( |
| | "Assembly", |
| | """C is a constant offset. |
| | VEL is the velocity or slope or gradient of the straight line. |
| | ACC is the acceleration or coefficient of the second order. The function is a parabola.""", |
| | ) |
| | ) |
| | self.help_label3.setToolTip( |
| | translate( |
| | "Assembly", |
| | """C is a constant offset. |
| | AMP is the amplitude of the sine wave. |
| | VEL is the angular velocity in radians per second. |
| | PHASE is the phase of the sine wave.""", |
| | ) |
| | ) |
| | self.help_label4.setToolTip( |
| | translate( |
| | "Assembly", |
| | """C is a constant. |
| | TIMEC is the time constant of the exponential function.""", |
| | ) |
| | ) |
| | self.help_label5.setToolTip( |
| | translate( |
| | "Assembly", |
| | """L1 is step level before time = T0. |
| | L2 is step level after time = T0. |
| | SLOPE defines the steepness of the transition between L1 and L2 about time = T0. Higher values gives sharper cornered steps. SLOPE = 1000 or greater are suitable.""", |
| | ) |
| | ) |
| | self.help_label6.setToolTip( |
| | translate( |
| | "Assembly", |
| | """H is the height of the impulse. |
| | T1 is the start of the impulse. |
| | T2 is the end of the impulse. |
| | SLOPE defines the steepness of the transition between 0 and H about time = T1 and T2. Higher values gives sharper cornered impulses. SLOPE = 1000 or greater are suitable.""", |
| | ) |
| | ) |
| | self.help_label7.setToolTip( |
| | translate( |
| | "Assembly", |
| | """This is similar to the square impulse but the top has a sloping ramp. It is good for building a smooth piecewise linear function by adding a series of these. |
| | T1 is the start of the impulse. |
| | T2 is the end of the impulse. |
| | H1 is the height at T1 at the beginning of the ramp. |
| | H2 is the height at T2 at the end of the ramp. |
| | SLOPE defines the steepness of the transition between 0 and H1 and H2 to 0 about time = T1 and T2 respectively. Higher values gives sharper cornered impulses. SLOPE = 1000 or greater are suitable.""", |
| | ) |
| | ) |
| |
|
| | self.help_label0.setWordWrap(True) |
| | self.help_label1.setWordWrap(True) |
| | self.help_label2.setWordWrap(True) |
| | self.help_label3.setWordWrap(True) |
| | self.help_label4.setWordWrap(True) |
| | self.help_label5.setWordWrap(True) |
| | self.help_label6.setWordWrap(True) |
| | self.help_label7.setWordWrap(True) |
| |
|
| | width = 1000 |
| | self.help_label0.setFixedWidth(width) |
| | self.help_label1.setFixedWidth(width) |
| | self.help_label2.setFixedWidth(width) |
| | self.help_label3.setFixedWidth(width) |
| | self.help_label4.setFixedWidth(width) |
| | self.help_label5.setFixedWidth(width) |
| | self.help_label6.setFixedWidth(width) |
| | self.help_label7.setFixedWidth(width) |
| |
|
| | self.help_label0.setVisible(False) |
| | self.help_label1.setVisible(False) |
| | self.help_label2.setVisible(False) |
| | self.help_label3.setVisible(False) |
| | self.help_label4.setVisible(False) |
| | self.help_label5.setVisible(False) |
| | self.help_label6.setVisible(False) |
| | self.help_label7.setVisible(False) |
| |
|
| | self.help_label1.setTextInteractionFlags(Qt.TextSelectableByMouse) |
| | self.help_label2.setTextInteractionFlags(Qt.TextSelectableByMouse) |
| | self.help_label3.setTextInteractionFlags(Qt.TextSelectableByMouse) |
| | self.help_label4.setTextInteractionFlags(Qt.TextSelectableByMouse) |
| | self.help_label5.setTextInteractionFlags(Qt.TextSelectableByMouse) |
| | self.help_label6.setTextInteractionFlags(Qt.TextSelectableByMouse) |
| | self.help_label7.setTextInteractionFlags(Qt.TextSelectableByMouse) |
| | |
| | self.help_button = QPushButton(translate("Assembly", "Help"), self.dialog) |
| |
|
| | |
| | def toggle_help(): |
| | show = not self.help_label1.isVisible() |
| | self.help_label0.setVisible(show) |
| | self.help_label1.setVisible(show) |
| | self.help_label2.setVisible(show) |
| | self.help_label3.setVisible(show) |
| | self.help_label4.setVisible(show) |
| | self.help_label5.setVisible(show) |
| | self.help_label6.setVisible(show) |
| | self.help_label7.setVisible(show) |
| |
|
| | if show: |
| | self.help_button.setText(translate("Assembly", "Hide help")) |
| | else: |
| | self.help_button.setText(translate("Assembly", "Help")) |
| |
|
| | self.positionDialog() |
| |
|
| | self.help_button.clicked.connect(toggle_help) |
| |
|
| | def positionDialog(self): |
| | self.dialog.adjustSize() |
| |
|
| | |
| | screen = QGuiApplication.screenAt(self.initialPos) |
| | screen_geometry = ( |
| | screen.availableGeometry() |
| | if screen |
| | else QApplication.primaryScreen().availableGeometry() |
| | ) |
| |
|
| | |
| | dialog_position = self.initialPos |
| |
|
| | |
| | if dialog_position.x() + self.dialog.width() > screen_geometry.right(): |
| | dialog_position.setX(screen_geometry.right() - self.dialog.width()) |
| | if dialog_position.y() + self.dialog.height() > screen_geometry.bottom(): |
| | dialog_position.setY(screen_geometry.bottom() - self.dialog.height()) |
| |
|
| | |
| | if dialog_position.x() < screen_geometry.left(): |
| | dialog_position.setX(screen_geometry.left()) |
| | if dialog_position.y() < screen_geometry.top(): |
| | dialog_position.setY(screen_geometry.top()) |
| |
|
| | |
| | self.dialog.move(dialog_position) |
| |
|
| | def setup_joint_combo(self): |
| | |
| |
|
| | self.joint_combo.clear() |
| |
|
| | jointTypes = ["Revolute", "Slider", "Cylindrical"] |
| |
|
| | joints = UtilsAssembly.getJointsOfType(self.assembly, jointTypes) |
| |
|
| | |
| | for joint in joints: |
| | joint_label = joint.Label |
| | joint_icon = QIcon(joint.ViewObject.Icon) |
| | self.joint_combo.addItem(joint_icon, joint_label, userData=joint) |
| |
|
| | |
| | if self.joint in joints: |
| | self.joint_combo.setCurrentText(self.joint.Label) |
| | elif len(joints) > 0: |
| | self.joint = joints[0] |
| |
|
| | def setup_motiontype_combo(self): |
| | self.motion_type_combo.clear() |
| |
|
| | if self.joint is None: |
| | return |
| |
|
| | if self.joint.JointType == "Revolute": |
| | types = ["Angular"] |
| | elif self.joint.JointType == "Slider": |
| | types = ["Linear"] |
| | else: |
| | types = ["Angular", "Linear"] |
| |
|
| | self.motion_type_combo.addItems(types) |
| |
|
| | |
| | if self.motionType in types: |
| | self.motion_type_combo.setCurrentText(self.motionType) |
| | else: |
| | |
| | self.motionType = types[0] |
| |
|
| | def exec_(self): |
| | return self.dialog.exec() |
| |
|
| |
|
| | |
| | class TaskAssemblyCreateSimulation(QtCore.QObject): |
| | def __init__(self, simFeaturePy=None): |
| | super().__init__() |
| | Gui.Selection.clearSelection() |
| |
|
| | self.assembly = UtilsAssembly.activeAssembly() |
| |
|
| | self.initialPlcs = UtilsAssembly.saveAssemblyPartsPlacements(self.assembly) |
| |
|
| | self.doc = self.assembly.Document |
| | self.gui_doc = Gui.getDocument(self.doc) |
| |
|
| | self.view = self.gui_doc.activeView() |
| |
|
| | if not self.assembly or not self.view or not self.doc: |
| | return |
| |
|
| | self.runKinematicsTimer = QtCore.QTimer() |
| | self.runKinematicsTimer.setSingleShot(True) |
| | self.runKinematicsTimer.timeout.connect(self.displayLastFrame) |
| |
|
| | self.animationTimer = QtCore.QTimer() |
| | self.animationTimer.setInterval(50) |
| | self.animationTimer.timeout.connect(self.playAnimation) |
| |
|
| | self.form = Gui.PySideUic.loadUi(":/panels/TaskAssemblyCreateSimulation.ui") |
| | self.form.motionList.installEventFilter(self) |
| | self.setSpinboxPrecision(self.form.TimeStartSpinBox, 9) |
| | self.setSpinboxPrecision(self.form.TimeEndSpinBox, 9) |
| | self.setSpinboxPrecision(self.form.TimeStepOutputSpinBox, 9) |
| | self.setSpinboxPrecision(self.form.GlobalErrorToleranceSpinBox, 9, App.Units.Length) |
| | self.form.motionList.itemDoubleClicked.connect(self.onItemDoubleClicked) |
| | self.form.TimeStartSpinBox.valueChanged.connect(self.onTimeStartChanged) |
| | self.form.TimeEndSpinBox.valueChanged.connect(self.onTimeEndChanged) |
| | self.form.TimeStepOutputSpinBox.valueChanged.connect(self.onTimeStepOutputChanged) |
| | self.form.GlobalErrorToleranceSpinBox.valueChanged.connect( |
| | self.onGlobalErrorToleranceChanged |
| | ) |
| | self.form.RunKinematicsButton.clicked.connect(self.runKinematics) |
| | self.form.frameSlider.valueChanged.connect(self.onFrameChanged) |
| | self.form.FramesPerSecondSpinBox.valueChanged.connect(self.onFramesPerSecondChanged) |
| | self.form.PlayBackwardButton.clicked.connect(self.animationTimerStartBackward) |
| | self.form.PlayForwardButton.clicked.connect(self.animationTimerStartForward) |
| | self.form.StepBackwardButton.clicked.connect(self.stepBackward) |
| | self.form.StepForwardButton.clicked.connect(self.stepForward) |
| | self.form.StopButton.clicked.connect(self.stopAnimation) |
| | self.form.AddButton.clicked.connect(self.addMotionClicked) |
| | self.form.RemoveButton.clicked.connect(self.deleteSelectedMotions) |
| | self.form.groupBox_player.hide() |
| |
|
| | if simFeaturePy: |
| | self.simFeaturePy = simFeaturePy |
| | App.setActiveTransaction("Edit " + simFeaturePy.Label + " Simulation") |
| | self.onMotionsChanged() |
| | else: |
| | App.setActiveTransaction("Create Simulation") |
| | self.createSimulationObject() |
| |
|
| | self.setUiInitialValues() |
| |
|
| | self.simFeaturePy.Proxy.setMotionsChangedCallback(self.onMotionsChanged) |
| |
|
| | self.currentFrm = 1 |
| | self.startFrm = 1 |
| | self.endFrm = 100 |
| | self.fps = 30 |
| | self.deltaTime = 1.0 / self.fps |
| | self.startTime = time.time() |
| | self.index = 0 |
| |
|
| | def setUiInitialValues(self): |
| | self.form.TimeStartSpinBox.setProperty("rawValue", self.simFeaturePy.aTimeStart.Value) |
| | self.form.TimeEndSpinBox.setProperty("rawValue", self.simFeaturePy.bTimeEnd.Value) |
| | self.form.TimeStepOutputSpinBox.setProperty( |
| | "rawValue", self.simFeaturePy.cTimeStepOutput.Value |
| | ) |
| | self.form.GlobalErrorToleranceSpinBox.setProperty( |
| | "rawValue", self.simFeaturePy.fGlobalErrorTolerance |
| | ) |
| | self.setFrameValue(0) |
| | self.form.FramesPerSecondSpinBox.setValue(self.simFeaturePy.jFramesPerSecond) |
| |
|
| | def setSpinboxPrecision(self, spinbox, precision, unit=App.Units.TimeSpan): |
| | q = App.Units.Quantity() |
| | q.Unit = unit |
| | q.Format = {"Precision": precision} |
| | spinbox.setProperty("value", q) |
| |
|
| | def accept(self): |
| | self.deactivate() |
| | UtilsAssembly.restoreAssemblyPartsPlacements(self.assembly, self.initialPlcs) |
| | App.closeActiveTransaction() |
| | return True |
| |
|
| | def reject(self): |
| | self.deactivate() |
| | App.closeActiveTransaction(True) |
| | return True |
| |
|
| | def deactivate(self): |
| | self.animationTimer.stop() |
| | self.simFeaturePy.Proxy.setMotionsChangedCallback(None) |
| | if Gui.Control.activeDialog(): |
| | Gui.Control.closeDialog() |
| |
|
| | def onTimeStartChanged(self, quantity): |
| | self.simFeaturePy.aTimeStart = self.form.TimeStartSpinBox.property("rawValue") |
| |
|
| | def onTimeEndChanged(self, quantity): |
| | self.simFeaturePy.bTimeEnd = self.form.TimeEndSpinBox.property("rawValue") |
| |
|
| | def onTimeStepOutputChanged(self, quantity): |
| | self.simFeaturePy.cTimeStepOutput = self.form.TimeStepOutputSpinBox.property("rawValue") |
| |
|
| | def onGlobalErrorToleranceChanged(self, quantity): |
| | self.simFeaturePy.fGlobalErrorTolerance = self.form.GlobalErrorToleranceSpinBox.property( |
| | "rawValue" |
| | ) |
| |
|
| | def onItemDoubleClicked(self, item): |
| | row = self.form.motionList.row(item) |
| | if row < len(self.simFeaturePy.Group): |
| | motion = self.simFeaturePy.Group[row] |
| | motion.ViewObject.Proxy.openEditDialog() |
| | self.onMotionsChanged() |
| |
|
| | def createSimulationObject(self): |
| | sim_group = UtilsAssembly.getSimulationGroup(self.assembly) |
| | self.simFeaturePy = sim_group.newObject("App::FeaturePython", "Simulation") |
| | Simulation(self.simFeaturePy) |
| | ViewProviderSimulation(self.simFeaturePy.ViewObject) |
| |
|
| | def createMotionObject(self, motionType, joint, formula): |
| | motion = self.assembly.newObject("App::FeaturePython", "Motion") |
| | Motion(motion, motionType, joint, formula) |
| | ViewProviderMotion(motion.ViewObject) |
| |
|
| | listOfMotions = self.simFeaturePy.Group |
| | listOfMotions.append(motion) |
| | self.simFeaturePy.Group = listOfMotions |
| |
|
| | def onMotionsChanged(self): |
| | self.form.motionList.clear() |
| | for motion in self.simFeaturePy.Group: |
| | self.form.motionList.addItem(motion.Label) |
| |
|
| | def runKinematics(self): |
| | self.assembly.generateSimulation(self.simFeaturePy) |
| | nFrms = self.assembly.numberOfFrames() |
| | self.form.frameSlider.setMaximum(nFrms - 1) |
| | self.setFrameValue(nFrms - 1) |
| | self.form.groupBox_player.show() |
| |
|
| | def onFrameChanged(self, val): |
| | self.assembly.updateForFrame(val) |
| | self.form.FrameLabel.setText(translate("Assembly", "Frame" + " " + str(val))) |
| | time = float(val * self.simFeaturePy.cTimeStepOutput) |
| | self.form.FrameTimeLabel.setText(f"{time:.2f} s") |
| |
|
| | def onFramesPerSecondChanged(self): |
| | self.simFeaturePy.jFramesPerSecond = self.form.FramesPerSecondSpinBox.value() |
| |
|
| | def playBackward(self): |
| | pass |
| |
|
| | def animationTimerStartForward(self): |
| | self.direction = 1 |
| | self.animationTimerStart() |
| |
|
| | def animationTimerStartBackward(self): |
| | self.direction = -1 |
| | self.animationTimerStart() |
| |
|
| | def animationTimerStart(self): |
| | self.animationTimer.stop() |
| | self.currentFrm = self.form.frameSlider.value() |
| | self.startFrm = 1 |
| | self.endFrm = self.form.frameSlider.maximum() |
| | if self.startFrm >= self.endFrm: |
| | return |
| |
|
| | self.fps = self.simFeaturePy.jFramesPerSecond |
| | self.deltaTime = 1.0 / self.fps |
| | self.startTime = time.time() |
| | self.index = self.currentFrm |
| | self.animationTimer.setInterval(self.deltaTime * 1000) |
| | self.animationTimer.start() |
| |
|
| | def playAnimation(self): |
| | range_ = self.endFrm - self.startFrm |
| | offset = self.currentFrm - self.startFrm |
| | count = int((time.time() - self.startTime) / self.deltaTime) |
| | self.index = ((self.direction * count + offset) % range_) + self.startFrm |
| | self.setFrameValue(self.index) |
| |
|
| | def displayLastFrame(self): |
| | nFrms = self.assembly.numberOfFrames() |
| | self.setFrameValue(nFrms - 1) |
| |
|
| | def stepBackward(self): |
| | self.animationTimer.stop() |
| |
|
| | nextFrm = self.form.frameSlider.value() - 1 |
| | if nextFrm < 1: |
| | nextFrm = self.form.frameSlider.maximum() |
| | self.setFrameValue(nextFrm) |
| |
|
| | def stepForward(self): |
| | self.animationTimer.stop() |
| |
|
| | nextFrm = self.form.frameSlider.value() + 1 |
| | if nextFrm > self.form.frameSlider.maximum(): |
| | nextFrm = 1 |
| | self.setFrameValue(nextFrm) |
| |
|
| | def setFrameValue(self, val): |
| | if val < 1: |
| | val = 1 |
| | if val > self.form.frameSlider.maximum(): |
| | val = self.form.frameSlider.maximum() |
| |
|
| | self.form.frameSlider.setValue(val) |
| |
|
| | def stopAnimation(self): |
| | self.animationTimer.stop() |
| |
|
| | def addMotionClicked(self): |
| | dialog = MotionEditDialog(self.assembly) |
| | if dialog.exec_(): |
| | self.createMotionObject(dialog.motionType, dialog.joint, dialog.formula) |
| |
|
| | |
| | def eventFilter(self, watched, event): |
| | if self.form is not None and watched == self.form.motionList: |
| | if event.type() == QtCore.QEvent.ShortcutOverride: |
| | if event.key() == QtCore.Qt.Key_Delete: |
| | event.accept() |
| | return True |
| | return False |
| |
|
| | elif event.type() == QtCore.QEvent.KeyPress: |
| | if event.key() == QtCore.Qt.Key_Delete: |
| | self.deleteSelectedMotions() |
| | return True |
| |
|
| | return super().eventFilter(watched, event) |
| |
|
| | def deleteSelectedMotions(self): |
| | selected_indexes = self.form.motionList.selectedIndexes() |
| | sorted_indexes = sorted(selected_indexes, key=lambda x: x.row(), reverse=True) |
| | for index in sorted_indexes: |
| | row = index.row() |
| | if row < len(self.simFeaturePy.Group): |
| | motion = self.simFeaturePy.Group[row] |
| | |
| | self.simFeaturePy.Group.remove(motion) |
| | |
| | motion.Document.removeObject(motion.Name) |
| |
|
| |
|
| | if App.GuiUp: |
| | Gui.addCommand("Assembly_CreateSimulation", CommandCreateSimulation()) |
| |
|