HUGE changes

- Added an about page that shows readme
- Added even more comments
- Added a playback feature
- 100x the pain and suffering!
This commit is contained in:
Daniel Carta
2024-07-26 13:59:15 -04:00
parent 6f9829f4f8
commit 99a298ec9b
5 changed files with 198 additions and 57 deletions
Vendored
BIN
View File
Binary file not shown.
+10 -11
View File
@@ -1,8 +1,8 @@
# autoPlanner # Auto Planner
(WIP) An auto creation tool for Ridgebotics 2025 (WIP) An auto creation tool for Ridgebotics 2025
### Install ### Install:
```shell ```shell
git clone https://https://github.com/Team4388/autoPlanner2025/tree/pyside git clone https://https://github.com/Team4388/autoPlanner2025/tree/pyside
@@ -10,10 +10,10 @@ cd autoPlanner2025
pip install -r requirements.txt pip install -r requirements.txt
python3 ./main.py python3 ./main.py
``` ```
#
## Usage:
### Usage: ### "Path Editor" Tab:
##### "Path Editor" Tab:
- Right click to add nodes - Right click to add nodes
- Left click on specific points to manipulate paths and nodes: - Left click on specific points to manipulate paths and nodes:
@@ -24,19 +24,18 @@ python3 ./main.py
- Double click on control points to make path, and robot movment continuous, while keeping the node clicked the at the same location (Smooth the path) - Double click on control points to make path, and robot movment continuous, while keeping the node clicked the at the same location (Smooth the path)
- Press the 'r' key to delete the whole auto - Press the 'r' key to delete the whole auto
##### "Button editor" Tab: ### "Button editor" Tab:
- Click on specific frames on the timeline to change to that position - Click on specific frames on the timeline to change to that position
- Pressing or holding the left or right arrow keys will move the current frame
- When selected on a frame, the robot's position in that time should show up. - When selected on a frame, the robot's position in that time should show up.
- Drag positional keyframes around to speed up and speed down the robot's travel between nodes - Drag positional keyframes around to speed up and speed down the robot's travel between nodes
- While a frame is selected, Press the 'e' key to swap to button mode. - Pressing the 'e' key will pause and unpause the auto playback
- In button mode select buttons on the driver and operator controllers.
##### "Export" Tab: ### "Export" Tab (not made yet):
- Click export, and save to a file - Click export, and save to a file
### Known Bugs: ### Known Bugs:
- Smoothing function is janky sometimes - Smoothing function is janky sometimes
- Sometimes the control points spawn in random locations but I cant seem to replicate it?
+43
View File
@@ -0,0 +1,43 @@
import sys
import os
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QScrollArea
from PySide6.QtGui import QPixmap
from PySide6.QtCore import Qt
class AboutWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("About")
self.setGeometry(100, 100, 700, 700)
# Create a central widget and set the layout
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
layout.addWidget(scroll_area)
# Create a widget to hold the content
content_widget = QWidget()
scroll_area.setWidget(content_widget)
content_layout = QVBoxLayout(content_widget)
# Read the README file
readme_path = os.path.join(os.path.dirname(__file__), 'README.md')
try:
with open(readme_path, 'r') as file:
readme_content = file.read()
except FileNotFoundError:
readme_content = "README.md file not found."
about_label = QLabel(readme_content)
about_label.setWordWrap(True)
about_label.setTextFormat(Qt.MarkdownText)
content_layout.addWidget(about_label)
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = AboutWindow()
main_window.show()
sys.exit(app.exec())
+97 -28
View File
@@ -2,8 +2,8 @@ import sys
import os import os
import numpy as np import numpy as np
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout, QWidget from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout, QWidget
from PySide6.QtGui import QPixmap, QPainter, QPen, QColor from PySide6.QtGui import QPixmap, QPainter, QPen, QColor, QKeyEvent
from PySide6.QtCore import Qt, QPoint, QRect from PySide6.QtCore import Qt, QPoint, QRect, QTimer
''' '''
This window is the button editor This window is the button editor
@@ -13,7 +13,8 @@ The button editor takes the path that is made in the path planner and lets the u
class ButtonEditor(QMainWindow): class ButtonEditor(QMainWindow):
def __init__(self, pathPlanner): def __init__(self, pathPlanner):
super().__init__() super().__init__()
# Initialization
# Initialization for the window
self.setWindowTitle("Button Editor") self.setWindowTitle("Button Editor")
self.pathPlanner = pathPlanner self.pathPlanner = pathPlanner
@@ -68,7 +69,8 @@ class ButtonEditor(QMainWindow):
self.matchTicks = self.matchLength * self.TPS # The amount of ticks in a match self.matchTicks = self.matchLength * self.TPS # The amount of ticks in a match
self.displayTickResolution = 6.25 # The number of ticks to divide the match ticks by self.displayTickResolution = 6.25 # The number of ticks to divide the match ticks by
self.displayTicks = round(self.matchTicks / self.displayTickResolution) # How many ticks to show the user self.displayTicks = round(self.matchTicks / self.displayTickResolution) # How many ticks to show the user
self.paused = True # If the user has paused the auto
self.playbackSpeed = 0.25 # Speed multiplier for playback
self.currentFrame = 1 # The frame that is currently selected self.currentFrame = 1 # The frame that is currently selected
self.keyFrameData = [{"isNode": False, "isButton": False} for _ in range(self.displayTicks)] # Dictionary for every tick including if that tick is a node frame or a button frame self.keyFrameData = [{"isNode": False, "isButton": False} for _ in range(self.displayTicks)] # Dictionary for every tick including if that tick is a node frame or a button frame
self.updateKeyFrameData() self.updateKeyFrameData()
@@ -82,10 +84,38 @@ class ButtonEditor(QMainWindow):
for idx in node_indices: for idx in node_indices:
self.keyFrameData[idx]["isNode"] = True self.keyFrameData[idx]["isNode"] = True
# Start the timer
self.timer = QTimer(self)
self.timer.timeout.connect(self.advanceFrame)
self.timer.start(int(1000 // (self.TPS * self.playbackSpeed)))
# Initialization # Initialization
self.setupFrames() self.setupFrames()
self.updateScene()
self.updateRectangles() self.updateRectangles()
self.updateTimeLabel() self.updateTimeLabel()
self.rectangleClicked(0)
# Tell when the user presses a key down
def keyPressEvent(self, event: QKeyEvent):
old_frame = self.currentFrame
if event.key() == Qt.Key_Right and self.currentFrame < len(self.displayFrames):
self.currentFrame += 1
elif event.key() == Qt.Key_Left and self.currentFrame > 1:
self.currentFrame -= 1
elif event.key() == Qt.Key_E:
self.paused = not self.paused
if self.paused:
self.timer.stop()
else:
self.timer.start(int(1000 // (self.TPS * self.playbackSpeed)))
print(self.paused)
if old_frame != self.currentFrame:
self.updateRectangles()
self.updateScene()
self.updateTimeLabel()
# Updating the key frames with data from the path planner # Updating the key frames with data from the path planner
def updateKeyFrameData(self): def updateKeyFrameData(self):
@@ -170,6 +200,25 @@ class ButtonEditor(QMainWindow):
start_angle = self.pathPlanner.nodeAngles[i] if i < len(self.pathPlanner.nodeAngles) else 0 start_angle = self.pathPlanner.nodeAngles[i] if i < len(self.pathPlanner.nodeAngles) else 0
end_angle = self.pathPlanner.nodeAngles[i + 1] if i + 1 < len(self.pathPlanner.nodeAngles) else 0 end_angle = self.pathPlanner.nodeAngles[i + 1] if i + 1 < len(self.pathPlanner.nodeAngles) else 0
angle = self.interpolateAngle(start_angle, end_angle, t) angle = self.interpolateAngle(start_angle, end_angle, t)
total_frames = self.displayTicks
frame_index = self.currentFrame - 1
if 0 <= frame_index < total_frames:
overall_t = frame_index / (total_frames - 1)
num_segments = len(self.pathPlanner.coordinates) - 1
segment_index = min(int(overall_t * num_segments), num_segments - 1)
segment_t = (overall_t * num_segments) - segment_index
start = QPoint(self.pathPlanner.coordinates[segment_index][0], self.pathPlanner.coordinates[segment_index][1])
end = QPoint(self.pathPlanner.coordinates[segment_index + 1][0], self.pathPlanner.coordinates[segment_index + 1][1])
control1 = self.pathPlanner.controlPoints[segment_index][0]
control2 = self.pathPlanner.controlPoints[segment_index][1]
point = self.pointOnBezierCurve(start, control1, control2, end, segment_t)
start_angle = self.pathPlanner.nodeAngles[segment_index] if segment_index < len(self.pathPlanner.nodeAngles) else 0
end_angle = self.pathPlanner.nodeAngles[segment_index + 1] if segment_index + 1 < len(self.pathPlanner.nodeAngles) else 0
angle = self.interpolateAngle(start_angle, end_angle, segment_t)
self.drawRobot(painter, point, angle) self.drawRobot(painter, point, angle)
painter.end() # If you don't end the painter the app crashes painter.end() # If you don't end the painter the app crashes
@@ -210,33 +259,40 @@ class ButtonEditor(QMainWindow):
# Update the frames on the bottom of the screen that the user interacts with # Update the frames on the bottom of the screen that the user interacts with
def updateRectangles(self): def updateRectangles(self):
# Add the key frame widget # Clear existing rectangles
for i in reversed(range(self.rectanglesLayout.count())): while self.rectanglesLayout.count():
widgetToRemove = self.rectanglesLayout.itemAt(i).widget() item = self.rectanglesLayout.takeAt(0)
self.rectanglesLayout.removeWidget(widgetToRemove) widget = item.widget()
widgetToRemove.setParent(None) if widget:
widget.deleteLater()
#Set the key frames width
window_width = self.rectanglesWidget.width() # Calculate the width of each rectangle
if self.keyFrameData: total_width = self.width() - 80
rect_width = window_width / len(self.keyFrameData) rect_width = max(1, total_width // self.displayTicks)
rect_height = 100 # Adjustable key frame height # Create new rectangles
for index, frame in enumerate(self.keyFrameData): for i in range(self.displayTicks):
rectWidget = QWidget() rect = QWidget()
rectWidget.setFixedSize(rect_width, rect_height) rect.setFixedSize(rect_width, 100)
# Set the colors of the frames # Set the color based on frame type
if frame["isNode"]: if i == self.currentFrame - 1:
rectWidget.setStyleSheet("background-color: yellow;") color = "red"
elif self.keyFrameData[i]["isNode"]:
color = "yellow"
elif self.keyFrameData[i]["isButton"]:
color = "blue"
else: else:
if index % 2 == 0: color = "#ADD8E6" if i % 2 == 0 else "#00008B"
rectWidget.setStyleSheet("background-color: #ADD8E6;") rect.setStyleSheet(f"background-color: {color};")
else:
rectWidget.setStyleSheet("background-color: #00008B;")
rectWidget.mousePressEvent = lambda event, idx=index: self.rectangleClicked(idx) # Connect the click event
self.rectanglesLayout.addWidget(rectWidget) rect.mousePressEvent = lambda event, index=i: self.rectangleClicked(index)
self.rectanglesLayout.addWidget(rect)
# Force layout update
self.rectanglesWidget.updateGeometry()
self.rectanglesLayout.update()
# Set the current frame to the frame that the user clicked # Set the current frame to the frame that the user clicked
def rectangleClicked(self, index): def rectangleClicked(self, index):
@@ -257,6 +313,19 @@ class ButtonEditor(QMainWindow):
milliseconds = int((current_time % 1) * 1000) milliseconds = int((current_time % 1) * 1000)
self.timeLabel.setText(f"{minutes}:{seconds:02d}.{milliseconds:03d} / {self.matchLength:.3f} sec") self.timeLabel.setText(f"{minutes}:{seconds:02d}.{milliseconds:03d} / {self.matchLength:.3f} sec")
# The auto playback
def advanceFrame(self):
if not self.paused:
if self.currentFrame < len(self.displayFrames):
self.currentFrame += 1
else:
self.currentFrame = 1 # Loop back to the start
# Update
self.updateRectangles()
self.updateScene()
self.updateTimeLabel()
# App initialization # App initialization
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
+48 -18
View File
@@ -5,79 +5,105 @@ from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QV
from PySide6.QtGui import QPixmap, QMouseEvent, QPainter, QPen, QColor, QPainterPath, QPolygon, QFont, QKeyEvent from PySide6.QtGui import QPixmap, QMouseEvent, QPainter, QPen, QColor, QPainterPath, QPolygon, QFont, QKeyEvent
from PySide6.QtCore import Qt, QPoint, QRect from PySide6.QtCore import Qt, QPoint, QRect
from buttonEditor import ButtonEditor from buttonEditor import ButtonEditor # Import the button editor
from about import AboutWindow
'''
This is the path planner window
The path planner lets the user add nodes and control points to let them change the bezier curves and the path of the robot during the auto
'''
class PathPlanner(QMainWindow): class PathPlanner(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("Path Planner") # Set the window title
self.setWindowTitle("Path Planner") # Set up the arrays
self.coordinates = np.empty((0, 2), dtype=int) # Make an empty array for the coordinates of objects
self.coordinates = np.empty((0, 2), dtype=int)
self.controlPoints = [] self.controlPoints = []
self.rotationHandles = [] self.rotationHandles = []
self.nodeAngles = [] self.nodeAngles = []
# Find the field png and then set the background to that png
self.imageLabel = QLabel(self) self.imageLabel = QLabel(self)
scriptDir = os.path.dirname(os.path.abspath(__file__)) scriptDir = os.path.dirname(os.path.abspath(__file__))
imagePath = os.path.join(scriptDir, "images", "Field.png") imagePath = os.path.join(scriptDir, "images", "Field.png")
self.pixmap = QPixmap(imagePath) self.pixmap = QPixmap(imagePath)
if self.pixmap.isNull(): if self.pixmap.isNull():
self.imageLabel.setText(f"Image not found at: {imagePath}") self.imageLabel.setText(f"Image not found at: {imagePath}")
else: else:
self.imageLabel.setPixmap(self.pixmap) self.imageLabel.setPixmap(self.pixmap)
# Buttons at the top of the screen
self.mainWindowButton = QPushButton("Main Window") self.mainWindowButton = QPushButton("Main Window")
self.mainWindowButton.clicked.connect(self.showMainWindow) self.mainWindowButton.clicked.connect(self.showMainWindow)
self.buttonEditorButton = QPushButton("Button Editor") self.buttonEditorButton = QPushButton("Button Editor")
self.buttonEditorButton.clicked.connect(self.showButtonEditor) self.buttonEditorButton.clicked.connect(self.showButtonEditor)
self.aboutButton = QPushButton("About")
self.aboutButton.clicked.connect(self.showAbout)
# Button layouts
buttonLayout = QHBoxLayout() buttonLayout = QHBoxLayout()
buttonLayout.addWidget(self.mainWindowButton) buttonLayout.addWidget(self.mainWindowButton)
buttonLayout.addWidget(self.buttonEditorButton) buttonLayout.addWidget(self.buttonEditorButton)
buttonLayout.addWidget(self.aboutButton)
# Adding the button layout to the main layout
layout = QVBoxLayout() layout = QVBoxLayout()
layout.addLayout(buttonLayout) layout.addLayout(buttonLayout)
layout.addWidget(self.imageLabel) layout.addWidget(self.imageLabel)
# Set the button editor to that script
self.buttonEditor = ButtonEditor(self) self.buttonEditor = ButtonEditor(self)
# Defining the overall layout
container = QWidget() container = QWidget()
container.setLayout(layout) container.setLayout(layout)
self.setCentralWidget(container) self.setCentralWidget(container)
# Resize the window to all of the layouts and widgets added
self.resize(self.pixmap.width(), self.pixmap.height() + 60) self.resize(self.pixmap.width(), self.pixmap.height() + 60)
self.setMouseTracking(True) # Let the app track the users mouse
self.setMouseTracking(True)
self.lastClickPos = QPoint() self.lastClickPos = QPoint()
self.coordinates = np.empty((0, 2), dtype=int) # Variables
self.lastClickTime = 0 self.coordinates = np.empty((0, 2), dtype=int) # Define the coordinate array again
self.nodeSize = 35 self.lastClickTime = 0 # The last time the user clicked on the sceen
self.handleSize = 15 self.nodeSize = 35 # How large to make the nodes
self.rotationHandleDistance = 35 self.handleSize = 15 # How large to make the rotation/control handles
self.controlPoints = [] self.rotationHandleDistance = 35 # How far the rotation handles are from the node
self.rotationHandles = [] self.controlPoints = [] # Stores all of the control points
self.nodeAngles = [] self.rotationHandles = [] # Stores all of the rotation handles
self.draggingControlPoint = False self.nodeAngles = [] # Stores all of the node angles
# Tell what the user is currently dragging
self.draggingControlPoint = False
self.draggingNode = False self.draggingNode = False
self.draggingRotationHandle = False self.draggingRotationHandle = False
self.draggingControlPointIndex = (-1, -1) self.draggingControlPointIndex = (-1, -1)
self.draggingNodeIndex = -1 self.draggingNodeIndex = -1
self.draggingRotationHandleIndex = -1 self.draggingRotationHandleIndex = -1
# Tell when the user presses a key down
def keyPressEvent(self, event: QKeyEvent): def keyPressEvent(self, event: QKeyEvent):
if event.key() == Qt.Key_R: if event.key() == Qt.Key_R:
# Clear the auto
self.showClearWarning() self.showClearWarning()
# Tell when the user presses a mouse button
def mousePressEvent(self, event: QMouseEvent): def mousePressEvent(self, event: QMouseEvent):
# Get the position of the mouse click
pos = self.imageLabel.mapFrom(self, event.position().toPoint()) pos = self.imageLabel.mapFrom(self, event.position().toPoint())
x, y = pos.x(), pos.y() x, y = pos.x(), pos.y()
# Tell if the mouse was click ed in the window
if 0 <= x < self.pixmap.width() and 0 <= y < self.pixmap.height(): if 0 <= x < self.pixmap.width() and 0 <= y < self.pixmap.height():
# If right clicked, add a node
if event.button() == Qt.RightButton: if event.button() == Qt.RightButton:
self.coordinates = np.vstack((self.coordinates, [x, y])) self.coordinates = np.vstack((self.coordinates, [x, y]))
self.nodeAngles.append(0) self.nodeAngles.append(0)
@@ -321,7 +347,11 @@ class PathPlanner(QMainWindow):
def showButtonEditor(self): def showButtonEditor(self):
self.hide() self.hide()
self.buttonEditor.show() self.buttonEditor.show()
def showAbout(self):
self.about_window = AboutWindow()
self.about_window.show()
if __name__ == "__main__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)