mirror of
https://github.com/Team4388/autoPlanner2025.git
synced 2026-06-08 16:28:07 -06:00
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:
@@ -1,8 +1,8 @@
|
||||
# autoPlanner
|
||||
# Auto Planner
|
||||
|
||||
(WIP) An auto creation tool for Ridgebotics 2025
|
||||
|
||||
### Install
|
||||
### Install:
|
||||
|
||||
```shell
|
||||
git clone https://https://github.com/Team4388/autoPlanner2025/tree/pyside
|
||||
@@ -10,10 +10,10 @@ cd autoPlanner2025
|
||||
pip install -r requirements.txt
|
||||
python3 ./main.py
|
||||
```
|
||||
#
|
||||
## Usage:
|
||||
|
||||
### Usage:
|
||||
|
||||
##### "Path Editor" Tab:
|
||||
### "Path Editor" Tab:
|
||||
|
||||
- Right click to add 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)
|
||||
- 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
|
||||
- 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.
|
||||
- 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.
|
||||
- In button mode select buttons on the driver and operator controllers.
|
||||
- Pressing the 'e' key will pause and unpause the auto playback
|
||||
|
||||
##### "Export" Tab:
|
||||
### "Export" Tab (not made yet):
|
||||
|
||||
- Click export, and save to a file
|
||||
|
||||
### Known Bugs:
|
||||
|
||||
- Smoothing function is janky sometimes
|
||||
- Sometimes the control points spawn in random locations but I cant seem to replicate it?
|
||||
- Smoothing function is janky sometimes
|
||||
@@ -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
@@ -2,8 +2,8 @@ import sys
|
||||
import os
|
||||
import numpy as np
|
||||
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout, QWidget
|
||||
from PySide6.QtGui import QPixmap, QPainter, QPen, QColor
|
||||
from PySide6.QtCore import Qt, QPoint, QRect
|
||||
from PySide6.QtGui import QPixmap, QPainter, QPen, QColor, QKeyEvent
|
||||
from PySide6.QtCore import Qt, QPoint, QRect, QTimer
|
||||
|
||||
'''
|
||||
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):
|
||||
def __init__(self, pathPlanner):
|
||||
super().__init__()
|
||||
# Initialization
|
||||
|
||||
# Initialization for the window
|
||||
self.setWindowTitle("Button Editor")
|
||||
self.pathPlanner = pathPlanner
|
||||
|
||||
@@ -68,7 +69,8 @@ class ButtonEditor(QMainWindow):
|
||||
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.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.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()
|
||||
@@ -82,10 +84,38 @@ class ButtonEditor(QMainWindow):
|
||||
for idx in node_indices:
|
||||
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
|
||||
self.setupFrames()
|
||||
self.updateScene()
|
||||
self.updateRectangles()
|
||||
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
|
||||
def updateKeyFrameData(self):
|
||||
@@ -170,6 +200,25 @@ class ButtonEditor(QMainWindow):
|
||||
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
|
||||
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)
|
||||
|
||||
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
|
||||
def updateRectangles(self):
|
||||
# Add the key frame widget
|
||||
for i in reversed(range(self.rectanglesLayout.count())):
|
||||
widgetToRemove = self.rectanglesLayout.itemAt(i).widget()
|
||||
self.rectanglesLayout.removeWidget(widgetToRemove)
|
||||
widgetToRemove.setParent(None)
|
||||
|
||||
#Set the key frames width
|
||||
window_width = self.rectanglesWidget.width()
|
||||
if self.keyFrameData:
|
||||
rect_width = window_width / len(self.keyFrameData)
|
||||
|
||||
rect_height = 100 # Adjustable key frame height
|
||||
for index, frame in enumerate(self.keyFrameData):
|
||||
rectWidget = QWidget()
|
||||
rectWidget.setFixedSize(rect_width, rect_height)
|
||||
# Clear existing rectangles
|
||||
while self.rectanglesLayout.count():
|
||||
item = self.rectanglesLayout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
widget.deleteLater()
|
||||
|
||||
# Calculate the width of each rectangle
|
||||
total_width = self.width() - 80
|
||||
rect_width = max(1, total_width // self.displayTicks)
|
||||
|
||||
# Create new rectangles
|
||||
for i in range(self.displayTicks):
|
||||
rect = QWidget()
|
||||
rect.setFixedSize(rect_width, 100)
|
||||
|
||||
# Set the colors of the frames
|
||||
if frame["isNode"]:
|
||||
rectWidget.setStyleSheet("background-color: yellow;")
|
||||
# Set the color based on frame type
|
||||
if i == self.currentFrame - 1:
|
||||
color = "red"
|
||||
elif self.keyFrameData[i]["isNode"]:
|
||||
color = "yellow"
|
||||
elif self.keyFrameData[i]["isButton"]:
|
||||
color = "blue"
|
||||
else:
|
||||
if index % 2 == 0:
|
||||
rectWidget.setStyleSheet("background-color: #ADD8E6;")
|
||||
else:
|
||||
rectWidget.setStyleSheet("background-color: #00008B;")
|
||||
color = "#ADD8E6" if i % 2 == 0 else "#00008B"
|
||||
rect.setStyleSheet(f"background-color: {color};")
|
||||
|
||||
rectWidget.mousePressEvent = lambda event, idx=index: self.rectangleClicked(idx)
|
||||
self.rectanglesLayout.addWidget(rectWidget)
|
||||
# Connect the click event
|
||||
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
|
||||
def rectangleClicked(self, index):
|
||||
@@ -257,6 +313,19 @@ class ButtonEditor(QMainWindow):
|
||||
milliseconds = int((current_time % 1) * 1000)
|
||||
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
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@@ -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.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):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Path Planner") # Set the window title
|
||||
|
||||
self.setWindowTitle("Path Planner")
|
||||
|
||||
self.coordinates = np.empty((0, 2), dtype=int)
|
||||
# Set up the arrays
|
||||
self.coordinates = np.empty((0, 2), dtype=int) # Make an empty array for the coordinates of objects
|
||||
self.controlPoints = []
|
||||
self.rotationHandles = []
|
||||
self.nodeAngles = []
|
||||
|
||||
# Find the field png and then set the background to that png
|
||||
self.imageLabel = QLabel(self)
|
||||
|
||||
scriptDir = os.path.dirname(os.path.abspath(__file__))
|
||||
imagePath = os.path.join(scriptDir, "images", "Field.png")
|
||||
self.pixmap = QPixmap(imagePath)
|
||||
|
||||
|
||||
if self.pixmap.isNull():
|
||||
self.imageLabel.setText(f"Image not found at: {imagePath}")
|
||||
else:
|
||||
self.imageLabel.setPixmap(self.pixmap)
|
||||
|
||||
# Buttons at the top of the screen
|
||||
self.mainWindowButton = QPushButton("Main Window")
|
||||
self.mainWindowButton.clicked.connect(self.showMainWindow)
|
||||
self.buttonEditorButton = QPushButton("Button Editor")
|
||||
self.buttonEditorButton.clicked.connect(self.showButtonEditor)
|
||||
self.aboutButton = QPushButton("About")
|
||||
self.aboutButton.clicked.connect(self.showAbout)
|
||||
|
||||
# Button layouts
|
||||
buttonLayout = QHBoxLayout()
|
||||
buttonLayout.addWidget(self.mainWindowButton)
|
||||
buttonLayout.addWidget(self.buttonEditorButton)
|
||||
buttonLayout.addWidget(self.aboutButton)
|
||||
|
||||
# Adding the button layout to the main layout
|
||||
layout = QVBoxLayout()
|
||||
layout.addLayout(buttonLayout)
|
||||
layout.addWidget(self.imageLabel)
|
||||
|
||||
# Set the button editor to that script
|
||||
self.buttonEditor = ButtonEditor(self)
|
||||
|
||||
# Defining the overall layout
|
||||
container = QWidget()
|
||||
container.setLayout(layout)
|
||||
self.setCentralWidget(container)
|
||||
|
||||
# Resize the window to all of the layouts and widgets added
|
||||
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.coordinates = np.empty((0, 2), dtype=int)
|
||||
self.lastClickTime = 0
|
||||
self.nodeSize = 35
|
||||
self.handleSize = 15
|
||||
self.rotationHandleDistance = 35
|
||||
self.controlPoints = []
|
||||
self.rotationHandles = []
|
||||
self.nodeAngles = []
|
||||
self.draggingControlPoint = False
|
||||
# Variables
|
||||
self.coordinates = np.empty((0, 2), dtype=int) # Define the coordinate array again
|
||||
self.lastClickTime = 0 # The last time the user clicked on the sceen
|
||||
self.nodeSize = 35 # How large to make the nodes
|
||||
self.handleSize = 15 # How large to make the rotation/control handles
|
||||
self.rotationHandleDistance = 35 # How far the rotation handles are from the node
|
||||
self.controlPoints = [] # Stores all of the control points
|
||||
self.rotationHandles = [] # Stores all of the rotation handles
|
||||
self.nodeAngles = [] # Stores all of the node angles
|
||||
|
||||
# Tell what the user is currently dragging
|
||||
self.draggingControlPoint = False
|
||||
self.draggingNode = False
|
||||
self.draggingRotationHandle = False
|
||||
self.draggingControlPointIndex = (-1, -1)
|
||||
self.draggingNodeIndex = -1
|
||||
self.draggingRotationHandleIndex = -1
|
||||
|
||||
# Tell when the user presses a key down
|
||||
def keyPressEvent(self, event: QKeyEvent):
|
||||
if event.key() == Qt.Key_R:
|
||||
# Clear the auto
|
||||
self.showClearWarning()
|
||||
|
||||
|
||||
|
||||
# Tell when the user presses a mouse button
|
||||
def mousePressEvent(self, event: QMouseEvent):
|
||||
# Get the position of the mouse click
|
||||
pos = self.imageLabel.mapFrom(self, event.position().toPoint())
|
||||
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 right clicked, add a node
|
||||
if event.button() == Qt.RightButton:
|
||||
self.coordinates = np.vstack((self.coordinates, [x, y]))
|
||||
self.nodeAngles.append(0)
|
||||
@@ -321,7 +347,11 @@ class PathPlanner(QMainWindow):
|
||||
|
||||
def showButtonEditor(self):
|
||||
self.hide()
|
||||
self.buttonEditor.show()
|
||||
self.buttonEditor.show()
|
||||
|
||||
def showAbout(self):
|
||||
self.about_window = AboutWindow()
|
||||
self.about_window.show()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
Reference in New Issue
Block a user