class Progress_show(QDialog, Ui_Dialog): """Progress dialog. + Batch execute function. + Interrupt function. """ def __init__(self, type_num: AlgorithmType, mechanismParams: Dict[str, Any], setting: Dict[str, Any], parent=None): super(Progress_show, self).__init__(parent) self.setupUi(self) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.rejected.connect(self.closeWork) self.mechanisms = [] #Batch label. if 'maxGen' in setting: self.limit = setting['maxGen'] self.batch_label.setText("{} generation(s)".format(self.limit) if self.limit > 0 else '∞') self.limit_mode = 'maxGen' elif 'minFit' in setting: self.limit = setting['minFit'] self.batch_label.setText("fitness less then {}".format(self.limit)) self.limit_mode = 'minFit' elif 'maxTime' in setting: self.limit = setting['maxTime'] self.batch_label.setText("{:02d}:{:02d}:{:02d}".format( self.limit // 3600, (self.limit % 3600) // 60, self.limit % 3600 % 60)) self.limit_mode = 'maxTime' self.loopTime.setEnabled(self.limit > 0) #Timer. self.time = 0 self.timer = QTimer(self) self.timer.setInterval(1000) self.timer.timeout.connect(self.setTime) #Worker thread. self.work = WorkerThread(type_num, mechanismParams, setting) self.work.progress_update.connect(self.setProgress) self.work.result.connect(self.getResult) self.work.done.connect(self.finish) @pyqtSlot(int, str) def setProgress(self, progress, fitness): """Progress bar will always full.""" value = progress + self.limit * self.work.currentLoop if (self.limit_mode in ('minFit', 'maxTime')) or self.limit == 0: self.progressBar.setMaximum(value) self.progressBar.setValue(value) self.fitness_label.setText(fitness) @pyqtSlot() def setTime(self): """Set time label.""" self.time += 1 self.time_label.setText("{:02d}:{:02d}:{:02d}".format( self.time // 3600, (self.time % 3600) // 60, self.time % 3600 % 60)) @pyqtSlot() def on_Start_clicked(self): """Start the proccess.""" loop = self.loopTime.value() self.progressBar.setMaximum(self.limit * loop) #Progress bar will show generations instead of percent. if (self.limit_mode in ('minFit', 'maxTime')) or self.limit == 0: self.progressBar.setFormat("%v generations") self.work.setLoop(loop) self.timer.start() self.work.start() self.Start.setEnabled(False) self.loopTime.setEnabled(False) self.Interrupt.setEnabled(True) @pyqtSlot(dict, float) def getResult(self, mechanism: Dict[str, Any], time_spand: float): """Get the result.""" self.mechanisms.append(mechanism) self.time_spand = time_spand @pyqtSlot() def finish(self): """Finish the proccess.""" self.timer.stop() self.accept() @pyqtSlot() def on_Interrupt_clicked(self): """Interrupt the proccess.""" if self.work.isRunning(): self.work.stop() print("The thread has been interrupted.") @pyqtSlot() def closeWork(self): """Close the thread.""" if self.work.isRunning(): self.work.stop() print("The thread has been canceled.")
class ProgressDialog(QDialog, Ui_Dialog): """Progress dialog. + Batch execute function. + Interrupt function. """ def __init__(self, type_num: AlgorithmType, mech_params: Dict[str, Any], setting: Dict[str, Any], parent): """Input the algorithm settings.""" super(ProgressDialog, self).__init__(parent) self.setupUi(self) self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint) self.rejected.connect(self.__close_work) self.mechanisms: List[Dict[str, Any]] = [] # Batch label. if 'maxGen' in setting: self.limit = setting['maxGen'] if self.limit > 0: self.batch_label.setText(f"{self.limit} generation(s)") else: self.batch_label.setText('∞') self.limit_mode = 'maxGen' elif 'minFit' in setting: self.limit = setting['minFit'] self.batch_label.setText(f"fitness less then {self.limit}") self.limit_mode = 'minFit' elif 'maxTime' in setting: self.limit = setting['maxTime'] self.batch_label.setText(f"{self.limit // 3600:02d}:" f"{self.limit % 3600 // 60:02d}:" f"{self.limit % 3600 % 60:02d}") self.limit_mode = 'maxTime' self.loopTime.setEnabled(self.limit > 0) # Timer. self.time = 0 self.timer = QTimer(self) self.timer.setInterval(1000) self.timer.timeout.connect(self.__set_time) self.time_spend = 0. # Worker thread. self.work = WorkerThread(type_num, mech_params, setting) self.work.progress_update.connect(self.__set_progress) self.work.result.connect(self.__get_result) self.work.done.connect(self.__finish) @pyqtSlot(int, str) def __set_progress(self, progress: int, fitness: str): """Progress bar will always full.""" value = progress + self.limit * self.work.currentLoop if self.limit_mode in {'minFit', 'maxTime'} or self.limit == 0: self.progressBar.setMaximum(value) self.progressBar.setValue(value) self.fitness_label.setText(fitness) @pyqtSlot() def __set_time(self): """Set time label.""" self.time += 1 self.time_label.setText(f"{self.time // 3600:02d}:" f"{self.time % 3600 // 60:02d}:" f"{self.time % 3600 % 60:02d}") @pyqtSlot(name='on_start_button_clicked') def __start(self): """Start the process.""" loop = self.loopTime.value() self.progressBar.setMaximum(self.limit * loop) if self.limit_mode in {'minFit', 'maxTime'} or self.limit == 0: # Progress bar will show generations instead of percent. self.progressBar.setFormat("%v generations") self.work.setLoop(loop) self.timer.start() self.work.start() self.start_button.setEnabled(False) self.loopTime.setEnabled(False) self.interrupt_button.setEnabled(True) @pyqtSlot(dict, float) def __get_result(self, mechanism: Dict[str, Any], time_spend: float): """Get the result.""" self.mechanisms.append(mechanism) self.time_spend += time_spend @pyqtSlot() def __finish(self): """Finish the process.""" self.timer.stop() self.accept() @pyqtSlot(name='on_interrupt_button_clicked') def __interrupt(self): """Interrupt the process.""" if self.work.isRunning(): self.work.stop() print("The thread has been interrupted.") @pyqtSlot() def __close_work(self): """Close the thread.""" if self.work.isRunning(): self.work.stop() print("The thread has been canceled.")
class InputsWidget(QWidget, Ui_Form): """There has following functions: + Function of mechanism variables settings. + Path recording. """ def __init__(self, parent): super(InputsWidget, self).__init__(parent) self.setupUi(self) #parent's pointer. self.freemode_button = parent.freemode_button self.EntitiesPoint = parent.EntitiesPoint self.EntitiesLink = parent.EntitiesLink self.MainCanvas = parent.MainCanvas self.resolve = parent.resolve self.reloadCanvas = parent.reloadCanvas self.outputTo = parent.outputTo self.ConflictGuide = parent.ConflictGuide self.DOF = lambda: parent.DOF self.rightInput = parent.rightInput self.CommandStack = parent.CommandStack #self widgets. self.dial = QDial() self.dial.setEnabled(False) self.dial.valueChanged.connect(self.__updateVar) self.dial_spinbox.valueChanged.connect(self.__setVar) self.inputs_dial_layout.addWidget(RotatableView(self.dial)) self.variable_stop.clicked.connect(self.variableValueReset) self.inputs_playShaft = QTimer(self) self.inputs_playShaft.setInterval(10) self.inputs_playShaft.timeout.connect(self.__changeIndex) self.variable_list.currentRowChanged.connect(self.__dialOk) '''Inputs record context menu + Copy data from Point{} + ... ''' self.record_list.customContextMenuRequested.connect( self.on_record_list_context_menu) self.popMenu_record_list = QMenu(self) self.pathData = {} def clear(self): self.pathData.clear() for i in range(self.record_list.count() - 1): self.record_list.takeItem(1) self.variable_list.clear() @pyqtSlot(tuple) def setSelection(self, selections): """Set one selection from canvas.""" self.joint_list.setCurrentRow(selections[0] if selections[0] in self. EntitiesPoint.selectedRows() else -1) @pyqtSlot() def clearSelection(self): """Clear the points selection.""" self.joint_list.setCurrentRow(-1) @pyqtSlot(int) def on_joint_list_currentRowChanged(self, row: int): """Change the point row from input widget.""" self.base_link_list.clear() if not row > -1: return if row not in self.EntitiesPoint.selectedRows(): self.EntitiesPoint.setSelections((row, ), False) for linkName in self.EntitiesPoint.item(row, 1).text().split(','): if not linkName: continue self.base_link_list.addItem(linkName) @pyqtSlot(int) def on_base_link_list_currentRowChanged(self, row: int): """Set the drive links from base link.""" self.drive_link_list.clear() if not row > -1: return inputs_point = self.joint_list.currentRow() linkNames = self.EntitiesPoint.item(inputs_point, 1).text().split(',') for linkName in linkNames: if linkName == self.base_link_list.currentItem().text(): continue self.drive_link_list.addItem(linkName) @pyqtSlot(int) def on_drive_link_list_currentRowChanged(self, row: int): """Set enable of 'add variable' button.""" if not row > -1: self.variable_list_add.setEnabled(False) return typeText = self.joint_list.currentItem().text().split()[0] self.variable_list_add.setEnabled(typeText == '[R]') @pyqtSlot() def on_variable_list_add_clicked(self): """Add inputs variable from click button.""" self.__addInputsVariable(self.joint_list.currentRow(), self.base_link_list.currentItem().text(), self.drive_link_list.currentItem().text()) def __addInputsVariable(self, point: int, base_link: str, drive_link: str): """Add variable with '->' sign.""" if not self.DOF() > 0: return for vlink in self.EntitiesLink.data(): if (vlink.name in {base_link, drive_link }) and (len(vlink.points) < 2): return name = 'Point{}'.format(point) vars = [ name, base_link, drive_link, "{:.02f}".format(self.__getLinkAngle(point, drive_link)) ] for n, base, drive, a in self.getInputsVariables(): if {base_link, drive_link} == {base, drive}: return self.CommandStack.beginMacro("Add variable of {}".format(name)) self.CommandStack.push(AddVariable('->'.join(vars), self.variable_list)) self.CommandStack.endMacro() def addInputsVariables(self, variables: Tuple[Tuple[int, str, str]]): """Add from database.""" for variable in variables: self.__addInputsVariable(*variable) @pyqtSlot(int) def __dialOk(self, p0=None): """Set the angle of base link and drive link.""" row = self.variable_list.currentRow() enabled = row > -1 rotatable = (enabled and not self.freemode_button.isChecked() and self.rightInput()) self.dial.setEnabled(rotatable) self.dial_spinbox.setEnabled(rotatable) self.oldVar = self.dial.value() / 100. self.variable_play.setEnabled(rotatable) self.variable_speed.setEnabled(rotatable) self.dial.setValue( float(self.variable_list.currentItem().text().split('->')[-1]) * 100 if enabled else 0) def variableExcluding(self, row: int = None): """Remove variable if the point was been deleted. Default: all. """ one_row = row is not None for i, variable in enumerate(self.getInputsVariables()): row_ = variable[0] #If this is not origin point any more. if one_row and (row != row_): continue self.CommandStack.beginMacro( "Remove variable of Point{}".format(row)) self.CommandStack.push(DeleteVariable(i, self.variable_list)) self.CommandStack.endMacro() @pyqtSlot() def on_variable_remove_clicked(self): """Remove and reset angle.""" row = self.variable_list.currentRow() if not row > -1: return reply = QMessageBox.question(self, "Remove variable", "Do you want to remove this variable?") if reply != QMessageBox.Yes: return self.variable_stop.click() self.CommandStack.beginMacro("Remove variable of Point{}".format(row)) self.CommandStack.push(DeleteVariable(row, self.variable_list)) self.CommandStack.endMacro() self.EntitiesPoint.getBackPosition() self.resolve() def __getLinkAngle(self, row: int, link: str) -> float: """Get the angle of base link and drive link.""" points = self.EntitiesPoint.dataTuple() links = self.EntitiesLink.dataTuple() link_names = [vlink.name for vlink in links] relate = links[link_names.index(link)].points base = points[row] drive = points[relate[relate.index(row) - 1]] return base.slopeAngle(drive) def getInputsVariables(self) -> Tuple[int, str, str, float]: """A generator use to get variables. [0]: point num [1]: base link [2]: drive link [3]: angle """ for row in range(self.variable_list.count()): variable = self.variable_list.item(row).text().split('->') variable[0] = int(variable[0].replace('Point', '')) variable[3] = float(variable[3]) yield tuple(variable) def inputCount(self) -> int: """Use to show input variable count.""" return self.variable_list.count() def inputPair(self) -> Tuple[int, int]: """Back as point number code.""" vlinks = { vlink.name: set(vlink.points) for vlink in self.EntitiesLink.data() } for vars in self.getInputsVariables(): points = vlinks[vars[2]].copy() points.remove(vars[0]) yield (vars[0], points.pop()) def variableReload(self): """Auto check the points and type.""" self.joint_list.clear() for i in range(self.EntitiesPoint.rowCount()): text = "[{}] Point{}".format( self.EntitiesPoint.item(i, 2).text(), i) self.joint_list.addItem(text) self.variableValueReset() @pyqtSlot(float) def __setVar(self, value): self.dial.setValue(int(value % 360 * 100)) @pyqtSlot(int) def __updateVar(self, value): """Update the value when rotating QDial.""" item = self.variable_list.currentItem() value /= 100. self.dial_spinbox.setValue(value) if item: itemText = item.text().split('->') itemText[-1] = "{:.02f}".format(value) item.setText('->'.join(itemText)) self.resolve() if (self.record_start.isChecked() and abs(self.oldVar - value) > self.record_interval.value()): self.MainCanvas.recordPath() self.oldVar = value def variableValueReset(self): """Reset the value of QDial.""" if self.inputs_playShaft.isActive(): self.variable_play.setChecked(False) self.inputs_playShaft.stop() self.EntitiesPoint.getBackPosition() for i, variable in enumerate(self.getInputsVariables()): point = variable[0] text = '->'.join([ 'Point{}'.format(point), variable[1], variable[2], "{:.02f}".format(self.__getLinkAngle(point, variable[2])) ]) self.variable_list.item(i).setText(text) self.__dialOk() self.resolve() @pyqtSlot(bool) def on_variable_play_toggled(self, toggled): """Triggered when play button was changed.""" self.dial.setEnabled(not toggled) self.dial_spinbox.setEnabled(not toggled) if toggled: self.inputs_playShaft.start() else: self.inputs_playShaft.stop() @pyqtSlot() def __changeIndex(self): """QTimer change index.""" index = self.dial.value() speed = self.variable_speed.value() extremeRebound = (self.ConflictGuide.isVisible() and self.extremeRebound.isChecked()) if extremeRebound: speed *= -1 self.variable_speed.setValue(speed) index += int(speed * 6 * (3 if extremeRebound else 1)) index %= self.dial.maximum() self.dial.setValue(index) @pyqtSlot(bool) def on_record_start_toggled(self, toggled): """Save to file path data.""" if toggled: self.MainCanvas.recordStart(int(360 / self.record_interval.value())) return path = self.MainCanvas.getRecordPath() name, ok = QInputDialog.getText(self, "Recording completed!", "Please input name tag:") if (not name) or (name in self.pathData): i = 0 while "Record_{}".format(i) in self.pathData: i += 1 QMessageBox.information(self, "Record", "The name tag is being used or empty.") name = "Record_{}".format(i) self.addPath(name, path) def addPath(self, name: str, path: Tuple[Tuple[float, float]]): """Add path function.""" self.CommandStack.beginMacro("Add {{Path: {}}}".format(name)) self.CommandStack.push( AddPath(self.record_list, name, self.pathData, path)) self.CommandStack.endMacro() self.record_list.setCurrentRow(self.record_list.count() - 1) def loadPaths(self, paths: Tuple[Tuple[Tuple[float, float]]]): """Add multiple path.""" for name, path in paths.items(): self.addPath(name, path) @pyqtSlot() def on_record_remove_clicked(self): """Remove path data.""" row = self.record_list.currentRow() if not row > 0: return self.CommandStack.beginMacro("Delete {{Path: {}}}".format( self.record_list.item(row).text())) self.CommandStack.push(DeletePath(row, self.record_list, self.pathData)) self.CommandStack.endMacro() self.record_list.setCurrentRow(self.record_list.count() - 1) self.reloadCanvas() @pyqtSlot(QListWidgetItem) def on_record_list_itemDoubleClicked(self, item): """View path data.""" name = item.text().split(":")[0] try: data = self.pathData[name] except KeyError: return reply = QMessageBox.question( self, "Path data", "This path data including {}.".format(", ".join( "Point{}".format(i) for i in range(len(data)) if data[i])), (QMessageBox.Save | QMessageBox.Close), QMessageBox.Close) if reply != QMessageBox.Save: return file_name = self.outputTo( "path data", ["Comma-Separated Values (*.csv)", "Text file (*.txt)"]) if not file_name: return with open(file_name, 'w', newline='') as stream: writer = csv.writer(stream) for point in data: for coordinate in point: writer.writerow(coordinate) writer.writerow(()) print("Output path data: {}".format(file_name)) @pyqtSlot(QPoint) def on_record_list_context_menu(self, point): """Show the context menu. Show path [0], [1], ... Or copy path coordinates. """ row = self.record_list.currentRow() if row > -1: action = self.popMenu_record_list.addAction("Show all") action.index = -1 name = self.record_list.item(row).text().split(":")[0] try: data = self.pathData[name] except KeyError: return for action_text in ("Show", "Copy data from"): self.popMenu_record_list.addSeparator() for i in range(len(data)): if data[i]: action = self.popMenu_record_list.addAction( "{} Point{}".format(action_text, i)) action.index = i action = self.popMenu_record_list.exec_( self.record_list.mapToGlobal(point)) if action: if "Copy data from" in action.text(): QApplication.clipboard().setText('\n'.join( "{},{}".format(x, y) for x, y in data[action.index])) elif "Show" in action.text(): if action.index == -1: self.record_show.setChecked(True) self.MainCanvas.setPathShow(action.index) self.popMenu_record_list.clear() @pyqtSlot() def on_record_show_clicked(self): """Show all paths or hide.""" if self.record_show.isChecked(): show = -1 else: show = -2 self.MainCanvas.setPathShow(show) @pyqtSlot(int) def on_record_list_currentRowChanged(self, row): """Reload the canvas when switch the path.""" if self.record_show.isChecked(): self.MainCanvas.setPathShow(-1) self.reloadCanvas() def currentPath(self): """Return current path data to main canvas. + No path. + Show path data. + Auto preview. """ row = self.record_list.currentRow() if row == -1: self.MainCanvas.setAutoPath(False) return () elif row > 0: self.MainCanvas.setAutoPath(False) name = self.record_list.item(row).text() return self.pathData.get(name.split(':')[0], ()) elif row == 0: self.MainCanvas.setAutoPath(True) return ()
class InputsWidget(QWidget, Ui_Form): """There has following functions: + Function of mechanism variables settings. + Path recording. """ about_to_resolve = Signal() def __init__(self, parent: MainWindowBase): super(InputsWidget, self).__init__(parent) self.setupUi(self) # parent's function pointer. self.free_move_button = parent.free_move_button self.EntitiesPoint = parent.entities_point self.EntitiesLink = parent.entities_link self.MainCanvas = parent.main_canvas self.solve = parent.solve self.reload_canvas = parent.reload_canvas self.output_to = parent.output_to self.conflict = parent.conflict self.dof = parent.dof self.right_input = parent.right_input self.CommandStack = parent.command_stack self.set_coords_as_current = parent.set_coords_as_current # Angle panel self.dial = QDial() self.dial.setStatusTip("Input widget of rotatable joint.") self.dial.setEnabled(False) self.dial.valueChanged.connect(self.__update_var) self.dial_spinbox.valueChanged.connect(self.__set_var) self.inputs_dial_layout.addWidget(RotatableView(self.dial)) # Angle panel available check self.variable_list.currentRowChanged.connect(self.__dial_ok) # Play button. action = QShortcut(QKeySequence("F5"), self) action.activated.connect(self.variable_play.click) self.variable_stop.clicked.connect(self.variable_value_reset) # Timer for play button. self.inputs_play_shaft = QTimer() self.inputs_play_shaft.setInterval(10) self.inputs_play_shaft.timeout.connect(self.__change_index) # Change the point coordinates with current position. self.update_pos.clicked.connect(self.set_coords_as_current) # Inputs record context menu self.pop_menu_record_list = QMenu(self) self.record_list.customContextMenuRequested.connect( self.__record_list_context_menu ) self.__path_data: Dict[str, Sequence[_Coord]] = {} def clear(self): """Clear function to reset widget status.""" self.__path_data.clear() for _ in range(self.record_list.count() - 1): self.record_list.takeItem(1) self.variable_list.clear() def __set_angle_mode(self): """Change to angle input.""" self.dial.setMinimum(0) self.dial.setMaximum(36000) self.dial_spinbox.setMinimum(0) self.dial_spinbox.setMaximum(360) def __set_unit_mode(self): """Change to unit input.""" self.dial.setMinimum(-50000) self.dial.setMaximum(50000) self.dial_spinbox.setMinimum(-500) self.dial_spinbox.setMaximum(500) def path_data(self): """Return current path data.""" return self.__path_data @Slot(tuple) def set_selection(self, selections: Sequence[int]): """Set one selection from canvas.""" self.joint_list.setCurrentRow(selections[0]) @Slot() def clear_selection(self): """Clear the points selection.""" self.driver_list.clear() self.joint_list.setCurrentRow(-1) @Slot(int, name='on_joint_list_currentRowChanged') def __update_relate_points(self, _: int): """Change the point row from input widget.""" self.driver_list.clear() item: Optional[QListWidgetItem] = self.joint_list.currentItem() if item is None: return p0 = _variable_int(item.text()) vpoints = self.EntitiesPoint.data_tuple() type_int = vpoints[p0].type if type_int == VJoint.R: for i, vpoint in enumerate(vpoints): if i == p0: continue if vpoints[p0].same_link(vpoint): if vpoints[p0].grounded() and vpoint.grounded(): continue self.driver_list.addItem(f"[{vpoint.type_str}] Point{i}") elif type_int in {VJoint.P, VJoint.RP}: self.driver_list.addItem(f"[{vpoints[p0].type_str}] Point{p0}") @Slot(int, name='on_driver_list_currentRowChanged') def __set_add_var_enabled(self, _: int): """Set enable of 'add variable' button.""" driver = self.driver_list.currentIndex() self.variable_add.setEnabled(driver != -1) @Slot(name='on_variable_add_clicked') def __add_inputs_variable(self, p0: Optional[int] = None, p1: Optional[int] = None): """Add variable with '->' sign.""" if p0 is None: item: Optional[QListWidgetItem] = self.joint_list.currentItem() if item is None: return p0 = _variable_int(item.text()) if p1 is None: item: Optional[QListWidgetItem] = self.driver_list.currentItem() if item is None: return p1 = _variable_int(item.text()) # Check DOF. if self.dof() <= self.input_count(): QMessageBox.warning( self, "Wrong DOF", "The number of variable must no more than degrees of freedom." ) return # Check same link. vpoints = self.EntitiesPoint.data_tuple() if not vpoints[p0].same_link(vpoints[p1]): QMessageBox.warning( self, "Wrong pair", "The base point and driver point should at the same link." ) return # Check repeated pairs. for p0_, p1_, a in self.input_pairs(): if {p0, p1} == {p0_, p1_} and vpoints[p0].type == VJoint.R: QMessageBox.warning( self, "Wrong pair", "There already have a same pair." ) return name = f'Point{p0}' self.CommandStack.beginMacro(f"Add variable of {name}") if p0 == p1: # One joint by offset. value = vpoints[p0].true_offset() else: # Two joints by angle. value = vpoints[p0].slope_angle(vpoints[p1]) self.CommandStack.push(AddVariable('->'.join(( name, f'Point{p1}', f"{value:.02f}", )), self.variable_list)) self.CommandStack.endMacro() def add_inputs_variables(self, variables: Sequence[Tuple[int, int]]): """Add from database.""" for p0, p1 in variables: self.__add_inputs_variable(p0, p1) @Slot() def __dial_ok(self): """Set the angle of base link and drive link.""" row = self.variable_list.currentRow() enabled = row > -1 rotatable = ( enabled and not self.free_move_button.isChecked() and self.right_input() ) self.dial.setEnabled(rotatable) self.dial_spinbox.setEnabled(rotatable) self.oldVar = self.dial.value() / 100. self.variable_play.setEnabled(rotatable) self.variable_speed.setEnabled(rotatable) item: Optional[QListWidgetItem] = self.variable_list.currentItem() if item is None: return expr = item.text().split('->') p0 = int(expr[0].replace('Point', '')) p1 = int(expr[1].replace('Point', '')) value = float(expr[2]) if p0 == p1: self.__set_unit_mode() else: self.__set_angle_mode() self.dial.setValue(value * 100 if enabled else 0) def variable_excluding(self, row: Optional[int] = None): """Remove variable if the point was been deleted. Default: all.""" one_row: bool = row is not None for i, (b, d, a) in enumerate(self.input_pairs()): # If this is not origin point any more. if one_row and (row != b): continue self.CommandStack.beginMacro(f"Remove variable of Point{row}") self.CommandStack.push(DeleteVariable(i, self.variable_list)) self.CommandStack.endMacro() @Slot(name='on_variable_remove_clicked') def remove_var(self, row: int = -1): """Remove and reset angle.""" if row == -1: row = self.variable_list.currentRow() if not row > -1: return self.variable_stop.click() self.CommandStack.beginMacro(f"Remove variable of Point{row}") self.CommandStack.push(DeleteVariable(row, self.variable_list)) self.CommandStack.endMacro() self.EntitiesPoint.get_back_position() self.solve() def interval(self) -> float: """Return interval value.""" return self.record_interval.value() def input_count(self) -> int: """Use to show input variable count.""" return self.variable_list.count() def input_pairs(self) -> Iterator[Tuple[int, int, float]]: """Back as point number code.""" for row in range(self.variable_list.count()): var = self.variable_list.item(row).text().split('->') p0 = int(var[0].replace('Point', '')) p1 = int(var[1].replace('Point', '')) angle = float(var[2]) yield (p0, p1, angle) def variable_reload(self): """Auto check the points and type.""" self.joint_list.clear() for i in range(self.EntitiesPoint.rowCount()): type_text = self.EntitiesPoint.item(i, 2).text() self.joint_list.addItem(f"[{type_text}] Point{i}") self.variable_value_reset() @Slot(float) def __set_var(self, value: float): self.dial.setValue(int(value * 100 % self.dial.maximum())) @Slot(int) def __update_var(self, value: int): """Update the value when rotating QDial.""" item = self.variable_list.currentItem() value /= 100. self.dial_spinbox.blockSignals(True) self.dial_spinbox.setValue(value) self.dial_spinbox.blockSignals(False) if item: item_text = item.text().split('->') item_text[-1] = f"{value:.02f}" item.setText('->'.join(item_text)) self.about_to_resolve.emit() if ( self.record_start.isChecked() and abs(self.oldVar - value) > self.record_interval.value() ): self.MainCanvas.record_path() self.oldVar = value def variable_value_reset(self): """Reset the value of QDial.""" if self.inputs_play_shaft.isActive(): self.variable_play.setChecked(False) self.inputs_play_shaft.stop() self.EntitiesPoint.get_back_position() vpoints = self.EntitiesPoint.data_tuple() for i, (p0, p1, a) in enumerate(self.input_pairs()): self.variable_list.item(i).setText('->'.join([ f'Point{p0}', f'Point{p1}', f"{vpoints[p0].slope_angle(vpoints[p1]):.02f}", ])) self.__dial_ok() self.solve() @Slot(bool, name='on_variable_play_toggled') def __play(self, toggled: bool): """Triggered when play button was changed.""" self.dial.setEnabled(not toggled) self.dial_spinbox.setEnabled(not toggled) if toggled: self.inputs_play_shaft.start() else: self.inputs_play_shaft.stop() if self.update_pos_option.isChecked(): self.set_coords_as_current() @Slot() def __change_index(self): """QTimer change index.""" index = self.dial.value() speed = self.variable_speed.value() extreme_rebound = ( self.conflict.isVisible() and self.extremeRebound.isChecked() ) if extreme_rebound: speed = -speed self.variable_speed.setValue(speed) index += int(speed * 6 * (3 if extreme_rebound else 1)) index %= self.dial.maximum() self.dial.setValue(index) @Slot(bool, name='on_record_start_toggled') def __start_record(self, toggled: bool): """Save to file path data.""" if toggled: self.MainCanvas.record_start(int( self.dial_spinbox.maximum() / self.record_interval.value() )) return path = self.MainCanvas.get_record_path() name, ok = QInputDialog.getText( self, "Recording completed!", "Please input name tag:" ) i = 0 name = name or f"Record_{i}" while name in self.__path_data: name = f"Record_{i}" i += 1 QMessageBox.information( self, "Record", "The name tag is being used or empty." ) self.add_path(name, path) def add_path(self, name: str, path: Sequence[_Coord]): """Add path function.""" self.CommandStack.beginMacro(f"Add {{Path: {name}}}") self.CommandStack.push(AddPath( self.record_list, name, self.__path_data, path )) self.CommandStack.endMacro() self.record_list.setCurrentRow(self.record_list.count() - 1) def load_paths(self, paths: Dict[str, Sequence[_Coord]]): """Add multiple path.""" for name, path in paths.items(): self.add_path(name, path) @Slot(name='on_record_remove_clicked') def __remove_path(self): """Remove path data.""" row = self.record_list.currentRow() if not row > 0: return name = self.record_list.item(row).text() self.CommandStack.beginMacro(f"Delete {{Path: {name}}}") self.CommandStack.push(DeletePath( row, self.record_list, self.__path_data )) self.CommandStack.endMacro() self.record_list.setCurrentRow(self.record_list.count() - 1) self.reload_canvas() @Slot(QListWidgetItem, name='on_record_list_itemDoubleClicked') def __path_dlg(self, item: QListWidgetItem): """View path data.""" name = item.text().split(":")[0] try: data = self.__path_data[name] except KeyError: return points_text = ", ".join(f"Point{i}" for i in range(len(data))) if QMessageBox.question( self, "Path data", f"This path data including {points_text}.", (QMessageBox.Save | QMessageBox.Close), QMessageBox.Close ) != QMessageBox.Save: return file_name = self.output_to( "path data", ["Comma-Separated Values (*.csv)", "Text file (*.txt)"] ) if not file_name: return with open(file_name, 'w', encoding='utf-8', newline='') as stream: writer = csv.writer(stream) for point in data: for coordinate in point: writer.writerow(coordinate) writer.writerow(()) logger.info(f"Output path data: {file_name}") @Slot(QPoint) def __record_list_context_menu(self, point): """Show the context menu. Show path [0], [1], ... Or copy path coordinates. """ row = self.record_list.currentRow() if not row > -1: return showall_action = self.pop_menu_record_list.addAction("Show all") showall_action.index = -1 copy_action = self.pop_menu_record_list.addAction("Copy as new") name = self.record_list.item(row).text().split(':')[0] try: data = self.__path_data[name] except KeyError: # Auto preview path. data = self.MainCanvas.Path.path showall_action.setEnabled(False) else: for action_text in ("Show", "Copy data from"): self.pop_menu_record_list.addSeparator() for i in range(len(data)): if data[i]: action = self.pop_menu_record_list.addAction( f"{action_text} Point{i}" ) action.index = i action_exec = self.pop_menu_record_list.exec( self.record_list.mapToGlobal(point) ) if action_exec: if action_exec == copy_action: # Copy path data. num = 0 name_copy = f"{name}_{num}" while name_copy in self.__path_data: name_copy = f"{name}_{num}" num += 1 self.add_path(name_copy, data) elif "Copy data from" in action_exec.text(): # Copy data to clipboard. QApplication.clipboard().setText('\n'.join( f"{x},{y}" for x, y in data[action_exec.index] )) elif "Show" in action_exec.text(): # Switch points enabled status. if action_exec.index == -1: self.record_show.setChecked(True) self.MainCanvas.set_path_show(action_exec.index) self.pop_menu_record_list.clear() @Slot(bool, name='on_record_show_toggled') def __set_path_show(self, toggled: bool): """Show all paths or hide.""" self.MainCanvas.set_path_show(-1 if toggled else -2) @Slot(int, name='on_record_list_currentRowChanged') def __set_path(self, _: int): """Reload the canvas when switch the path.""" if not self.record_show.isChecked(): self.record_show.setChecked(True) self.reload_canvas() def current_path(self): """Return current path data to main canvas. + No path. + Show path data. + Auto preview. """ row = self.record_list.currentRow() if row in {0, -1}: return () path_name = self.record_list.item(row).text().split(':')[0] return self.__path_data.get(path_name, ()) @Slot(name='on_variable_up_clicked') @Slot(name='on_variable_down_clicked') def __set_variable_priority(self): row = self.variable_list.currentRow() if not row > -1: return item = self.variable_list.currentItem() self.variable_list.insertItem( row + (-1 if self.sender() == self.variable_up else 1), self.variable_list.takeItem(row) ) self.variable_list.setCurrentItem(item)