コード例 #1
0
 def __init__(self, mechanism, Path, parent=None):
     super(DynamicCanvas, self).__init__(parent)
     self.mechanism = mechanism
     self.Path.path = Path
     self.length = 0
     for path in self.Path.path:
         l = len(path)
         if l > self.length:
             self.length = l
     self.targetPath = self.mechanism['Target']
     self.index = 0
     #exp_symbol = ('A', 'B', 'C', 'D', 'E')
     self.exp_symbol = set()
     self.links = []
     for exp in self.mechanism['Link_Expression'].split(';'):
         names = strbetween(exp, '[', ']').split(',')
         self.links.append(tuple(names))
         for name in names:
             self.exp_symbol.add(name)
     self.exp_symbol = sorted(self.exp_symbol,
                              key=lambda e: int(e.replace('P', '')))
     #Error
     self.ERROR = False
     self.no_error = 0
     #Timer start.
     timer = QTimer(self)
     timer.timeout.connect(self.change_index)
     timer.start(17)
コード例 #2
0
ファイル: tables.py プロジェクト: se-forex/Pyslvs-PyQt5
class FPSLabel(QLabel):
    """This QLabel can show FPS of main canvas in status bar."""
    def __init__(self, parent: QWidget):
        super(FPSLabel, self).__init__(parent)
        self.__t0 = time() - 1
        self.__frame_timer = QTimer(self)
        self.__frame_timer.timeout.connect(self.__updateText)
        self.__frame_timer.start(500)

    @pyqtSlot()
    def __updateText(self):
        """Update FPS with timer."""
        t1 = time() - self.__t0
        fps = 1 / t1 if t1 else 1
        self.setText(f"FPS: {fps:6.02f}")

    @pyqtSlot()
    def updateText(self):
        """Update FPS with timer."""
        self.__updateText()
        self.__t0 = time()
コード例 #3
0
ファイル: preview.py プロジェクト: scrum-1/Pyslvs-PyQt5
 def __init__(self, mechanism, Path, parent=None):
     super(DynamicCanvas, self).__init__(parent)
     self.mechanism = mechanism
     self.Path.path = Path
     self.targetPath = self.mechanism['Target']
     self.index = 0
     #exp_symbol = ('A', 'B', 'C', 'D', 'E')
     self.exp_symbol = []
     self.links = []
     for exp in self.mechanism['Link_Expression'].split(';'):
         tags = get_from_parenthesis(exp, '[', ']').split(',')
         self.links.append(tuple(tags))
         for name in tags:
             if name not in self.exp_symbol:
                 self.exp_symbol.append(name)
     self.exp_symbol = sorted(self.exp_symbol)
     #Timer start.
     timer = QTimer(self)
     timer.setInterval(10)
     timer.timeout.connect(self.change_index)
     timer.start()
コード例 #4
0
class _DynamicCanvas(BaseCanvas):

    """Custom canvas for preview algorithm result."""

    def __init__(
        self,
        mechanism: Dict[str, Any],
        path: Sequence[Sequence[Tuple[float, float]]],
        parent: QWidget
    ):
        """Input link and path data."""
        super(_DynamicCanvas, self).__init__(parent)
        self.mechanism = mechanism
        self.Path.path = path
        self.target_path = self.mechanism['Target']
        self.__index = 0
        self.__interval = 1
        self.__path_count = max(len(path) for path in self.Path.path) - 1
        self.pos: List[Tuple[float, float]] = []

        # exp_symbol = {'P1', 'P2', 'P3', ...}
        exp_symbol = set()
        self.links = []
        for exp in self.mechanism['Link_expr'].split(';'):
            names = str_between(exp, '[', ']').split(',')
            self.links.append(tuple(names))
            for name in names:
                exp_symbol.add(name)
        self.exp_symbol = sorted(exp_symbol, key=lambda e: int(e.replace('P', '')))
        # Error
        self.error = False
        self.__no_error = 0
        # Timer start.
        self.__timer = QTimer(self)
        self.__timer.timeout.connect(self.__change_index)
        self.__timer.start(18)

    def __zoom_to_fit_limit(self) -> Tuple[float, float, float, float]:
        """Limitations of four side."""
        inf = float('inf')
        x_right = inf
        x_left = -inf
        y_top = -inf
        y_bottom = inf
        # Points
        for name in self.exp_symbol:
            x, y = self.mechanism[name]
            if x < x_right:
                x_right = x
            if x > x_left:
                x_left = x
            if y < y_bottom:
                y_bottom = y
            if y > y_top:
                y_top = y
        # Paths
        for i, path in enumerate(self.Path.path):
            if self.Path.show != -1 and self.Path.show != i:
                continue
            for x, y in path:
                if x < x_right:
                    x_right = x
                if x > x_left:
                    x_left = x
                if y < y_bottom:
                    y_bottom = y
                if y > y_top:
                    y_top = y
        # Solving paths
        for path in self.target_path.values():
            for x, y in path:
                if x < x_right:
                    x_right = x
                if x > x_left:
                    x_left = x
                if y < y_bottom:
                    y_bottom = y
                if y > y_top:
                    y_top = y
        return x_right, x_left, y_top, y_bottom

    def paintEvent(self, event):
        """Drawing functions."""
        width = self.width()
        height = self.height()
        x_right, x_left, y_top, y_bottom = self.__zoom_to_fit_limit()
        x_diff = x_left - x_right
        y_diff = y_top - y_bottom
        x_diff = x_diff if x_diff else 1
        y_diff = y_diff if y_diff else 1
        if width / x_diff < height / y_diff:
            factor = width / x_diff
        else:
            factor = height / y_diff
        self.zoom = factor * 0.95
        self.ox = width / 2 - (x_left + x_right) / 2 * self.zoom
        self.oy = height / 2 + (y_top + y_bottom) / 2 * self.zoom
        super(_DynamicCanvas, self).paintEvent(event)

        # First check.
        for path in self.Path.path:
            if not path:
                continue
            x, y = path[self.__index]
            if isnan(x):
                self.__index, self.__no_error = self.__no_error, self.__index
                self.error = True
                self.__interval = -self.__interval

        # Points that in the current angle section.
        self.pos.clear()
        for i, name in enumerate(self.exp_symbol):
            if (name in self.mechanism['Driver']) or (name in self.mechanism['Follower']):
                self.pos.append(self.mechanism[name])
            else:
                x, y = self.Path.path[i][self.__index]
                self.pos.append((x, y))

        # Draw links.
        for i, exp in enumerate(self.links):
            if i == 0:
                continue
            self.__draw_link(f"link_{i}", [self.exp_symbol.index(tag) for tag in exp])

        # Draw path.
        self.__draw_path()

        # Draw solving path.
        self.drawTargetPath()

        # Draw points.
        for i, name in enumerate(self.exp_symbol):
            if not self.pos[i]:
                continue
            self.__draw_point(i, name)

        self.painter.end()

        if self.error:
            self.error = False
            self.__index, self.__no_error = self.__no_error, self.__index
        else:
            self.__no_error = self.__index

    def __draw_point(self, i: int, name: str):
        """Draw point function."""
        x, y = self.pos[i]
        color = color_qt('Green')
        fixed = False
        if name in self.mechanism['Target']:
            color = color_qt('Dark-Orange')
        elif name in self.mechanism['Driver']:
            color = color_qt('Red')
            fixed = True
        elif name in self.mechanism['Follower']:
            color = color_qt('Blue')
            fixed = True
        self.drawPoint(i, x, y, fixed, color)

    def __draw_link(self, name: str, points: List[int]):
        """Draw link function.

        The link color will be the default color.
        """
        color = color_qt('Blue')
        pen = QPen(color)
        pen.setWidth(self.link_width)
        self.painter.setPen(pen)
        brush = QColor(226, 219, 190)
        brush.setAlphaF(0.70)
        self.painter.setBrush(brush)
        qpoints = tuple(
            QPointF(self.pos[i][0], -self.pos[i][1]) * self.zoom
            for i in points if self.pos[i] and (not isnan(self.pos[i][0]))
        )
        if len(qpoints) == len(points):
            self.painter.drawPolygon(*qpoints)
        self.painter.setBrush(Qt.NoBrush)
        if self.show_point_mark and (name != 'ground') and qpoints:
            pen.setColor(Qt.darkGray)
            self.painter.setPen(pen)
            self.painter.setFont(QFont('Arial', self.font_size))
            text = f"[{name}]"
            cen_x = sum(self.pos[i][0] for i in points if self.pos[i])
            cen_y = sum(self.pos[i][1] for i in points if self.pos[i])
            self.painter.drawText(QPointF(cen_x, -cen_y) * self.zoom / len(points), text)

    def __draw_path(self):
        """Draw a path.

        A simple function than main canvas.
        """
        pen = QPen()
        for i, path in enumerate(self.Path.path):
            color = color_qt('Green')
            if self.exp_symbol[i] in self.mechanism['Target']:
                color = color_qt('Dark-Orange')
            pen.setColor(color)
            pen.setWidth(self.path_width)
            self.painter.setPen(pen)
            self.drawCurve(path)

    @pyqtSlot()
    def __change_index(self):
        """A slot to change the path index."""
        self.__index += self.__interval
        if self.__index > self.__path_count:
            self.__index = 0
        self.update()
コード例 #5
0
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.")
コード例 #6
0
ファイル: inputs.py プロジェクト: s40523219/Pyslvs-PyQt5
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 ()
コード例 #7
0
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.")
コード例 #8
0
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)
コード例 #9
0
class _DynamicCanvas(BaseCanvas):
    """Custom canvas for preview algorithm result."""
    def __init__(self, mechanism: Dict[str, Any],
                 path: Sequence[Sequence[Tuple[float, float]]],
                 vpoints: List[VPoint], vlinks: List[VLink], parent: QWidget):
        """Input link and path data."""
        super(_DynamicCanvas, self).__init__(parent)
        self.mechanism = mechanism
        self.path.path = path
        self.vpoints = vpoints
        self.vlinks = vlinks
        ranges: Dict[int, Tuple[float, float,
                                float]] = self.mechanism['Placement']
        self.ranges.update({
            f"P{i}":
            QRectF(QPointF(values[0] - values[2], values[1] + values[2]),
                   QSizeF(values[2] * 2, values[2] * 2))
            for i, values in ranges.items()
        })
        same: Dict[int, int] = self.mechanism['same']
        target_path: Dict[int, List[Tuple[float,
                                          float]]] = self.mechanism['Target']
        for i, path in target_path.items():
            for j in range(i):
                if j in same:
                    i -= 1
            self.target_path[f"P{i}"] = path
        self.__index = 0
        self.__interval = 1
        self.__path_count = max(len(path) for path in self.path.path) - 1
        self.pos: List[Tuple[float, float]] = []

        # Error
        self.error = False
        self.__no_error = 0

        # Timer start.
        self.__timer = QTimer()
        self.__timer.timeout.connect(self.__change_index)
        self.__timer.start(18)

    def __zoom_to_fit_limit(self) -> Tuple[float, float, float, float]:
        """Limitations of four side."""
        inf = float('inf')
        x_right = -inf
        x_left = inf
        y_top = -inf
        y_bottom = inf
        # Paths
        for i, path in enumerate(self.path.path):
            for x, y in path:
                if x > x_right:
                    x_right = x
                if x < x_left:
                    x_left = x
                if y < y_bottom:
                    y_bottom = y
                if y > y_top:
                    y_top = y
        # Solving paths
        for path in self.target_path.values():
            for x, y in path:
                if x > x_right:
                    x_right = x
                if x < x_left:
                    x_left = x
                if y < y_bottom:
                    y_bottom = y
                if y > y_top:
                    y_top = y
        # Ranges
        for rect in self.ranges.values():
            if rect.right() > x_right:
                x_right = rect.right()
            if rect.left() < x_left:
                x_left = rect.left()
            if rect.bottom() < y_bottom:
                y_bottom = rect.bottom()
            if rect.top() > y_top:
                y_top = rect.top()
        return x_right, x_left, y_top, y_bottom

    def paintEvent(self, event):
        """Drawing functions."""
        width = self.width()
        height = self.height()
        x_right, x_left, y_top, y_bottom = self.__zoom_to_fit_limit()
        x_diff = x_right - x_left or 1.
        y_diff = y_top - y_bottom or 1.
        if width / x_diff < height / y_diff:
            self.zoom = width / x_diff * 0.95
        else:
            self.zoom = height / y_diff * 0.95
        self.ox = width / 2 - (x_left + x_right) / 2 * self.zoom
        self.oy = height / 2 + (y_top + y_bottom) / 2 * self.zoom
        super(_DynamicCanvas, self).paintEvent(event)

        # First check.
        for path in self.path.path:
            if not path:
                continue
            x, y = path[self.__index]
            if not isnan(x):
                continue
            self.__index, self.__no_error = self.__no_error, self.__index
            self.error = True
            self.__interval = -self.__interval

        # Points that in the current angle section.
        self.pos.clear()
        for i in range(len(self.vpoints)):
            if i in self.mechanism['Placement']:
                self.pos.append(self.vpoints[i].c[0])
            else:
                x, y = self.path.path[i][self.__index]
                self.pos.append((x, y))

        # Draw links.
        for vlink in self.vlinks:
            if vlink.name == 'ground':
                continue
            self.__draw_link(vlink.name, vlink.points)

        # Draw path.
        self.__draw_path()

        # Draw solving path.
        self.draw_target_path()
        self.draw_slvs_ranges()

        # Draw points.
        for i in range(len(self.vpoints)):
            if not self.pos[i]:
                continue
            self.__draw_point(i)

        self.painter.end()

        if self.error:
            self.error = False
            self.__index, self.__no_error = self.__no_error, self.__index
        else:
            self.__no_error = self.__index

    def __draw_point(self, i: int):
        """Draw point function."""
        k = i
        for j in range(i):
            if j in self.mechanism['same']:
                k += 1
        x, y = self.pos[i]
        color = color_rgb('Green')
        fixed = False
        if f"P{i}" in self.target_path:
            color = color_rgb('Dark-Orange')
        elif k in self.mechanism['Placement']:
            color = color_rgb('Blue')
            fixed = True
        self.draw_point(i, x, y, fixed, color)

    def __draw_link(self, name: str, points: Sequence[int]):
        """Draw link function.

        The link color will be the default color.
        """
        color = color_qt('Blue')
        pen = QPen(color)
        pen.setWidth(self.link_width)
        self.painter.setPen(pen)
        brush = QColor(226, 219, 190)
        brush.setAlphaF(0.70)
        self.painter.setBrush(brush)
        qpoints = tuple(
            QPointF(self.pos[i][0], -self.pos[i][1]) * self.zoom
            for i in points if self.pos[i] and (not isnan(self.pos[i][0])))
        if len(qpoints) == len(points):
            self.painter.drawPolygon(*qpoints)
        self.painter.setBrush(Qt.NoBrush)
        if self.show_point_mark and name != 'ground' and qpoints:
            pen.setColor(Qt.darkGray)
            self.painter.setPen(pen)
            self.painter.setFont(QFont('Arial', self.font_size))
            text = f"[{name}]"
            cen_x = sum(self.pos[i][0] for i in points if self.pos[i])
            cen_y = sum(self.pos[i][1] for i in points if self.pos[i])
            self.painter.drawText(
                QPointF(cen_x, -cen_y) * self.zoom / len(points), text)

    def __draw_path(self):
        """Draw a path.

        A simple function than main canvas.
        """
        pen = QPen()
        for i, path in enumerate(self.path.path):
            color = color_qt('Green')
            if f"P{i}" in self.target_path:
                color = color_qt('Dark-Orange')
            pen.setColor(color)
            pen.setWidth(self.path_width)
            self.painter.setPen(pen)
            self.draw_curve(path)

    @Slot()
    def __change_index(self):
        """A slot to change the path index."""
        self.__index += self.__interval
        if self.__index > self.__path_count:
            self.__index = 0
        self.update()