From f09ceafe369729ec1b023b0348dc2eae5a472e84 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 6 Feb 2025 20:35:43 -0800 Subject: [PATCH] Completed Meep Meep Drawing Feature --- src/main/java/MeepMeepDrawing/DrawTool.java | 63 ++ .../java/MeepMeepDrawing/DrawingScreen.java | 136 ++++ .../java/MeepMeepDrawing/MeepMeepDrawer.java | 584 ++++++++++++++++++ src/main/java/MeepMeepDrawing/Node.java | 141 +++++ src/main/java/MeepMeepDrawing/RobotPath.java | 276 +++++++++ src/main/java/MeepMeepDrawing/SelectTool.java | 61 ++ src/main/java/MeepMeepDrawing/Tool.java | 328 ++++++++++ .../MeepMeepDrawing/nodeEditing/MoveTool.java | 39 ++ .../nodeEditing/PathEditingTool.java | 55 ++ .../nodeEditing/RotateHeadingTool.java | 45 ++ .../nodeEditing/RotateTangentTool.java | 44 ++ 11 files changed, 1772 insertions(+) create mode 100644 src/main/java/MeepMeepDrawing/DrawTool.java create mode 100644 src/main/java/MeepMeepDrawing/DrawingScreen.java create mode 100644 src/main/java/MeepMeepDrawing/MeepMeepDrawer.java create mode 100644 src/main/java/MeepMeepDrawing/Node.java create mode 100644 src/main/java/MeepMeepDrawing/RobotPath.java create mode 100644 src/main/java/MeepMeepDrawing/SelectTool.java create mode 100644 src/main/java/MeepMeepDrawing/Tool.java create mode 100644 src/main/java/MeepMeepDrawing/nodeEditing/MoveTool.java create mode 100644 src/main/java/MeepMeepDrawing/nodeEditing/PathEditingTool.java create mode 100644 src/main/java/MeepMeepDrawing/nodeEditing/RotateHeadingTool.java create mode 100644 src/main/java/MeepMeepDrawing/nodeEditing/RotateTangentTool.java diff --git a/src/main/java/MeepMeepDrawing/DrawTool.java b/src/main/java/MeepMeepDrawing/DrawTool.java new file mode 100644 index 0000000..7f8244b --- /dev/null +++ b/src/main/java/MeepMeepDrawing/DrawTool.java @@ -0,0 +1,63 @@ +package MeepMeepDrawing; + +import com.noahbres.meepmeep.roadrunner.entity.RoadRunnerBotEntity; + +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; + +public class DrawTool extends Tool { + + + public DrawTool(RobotPath.PathType pathType, RoadRunnerBotEntity bot) { + super(pathType, bot); + } + + + + + @Override + public void mousePressed(MouseEvent e) { + super.mousePressed(e); + //ensures that only left click applies the action + if (e.getButton()!=MouseEvent.BUTTON1) + return; + //initializes the starting path if there is no other path drawn yet + if (getPath().isEmpty()) { + initPath(e.getPoint()); + return; + } + + //makes a new path with the starting node being the ending node of the previous path + Node previousNode = getPath().get(getPath().size()-1).getN2(); + RobotPath newPath = new RobotPath(previousNode, new Node(e.getPoint(), 0,0), super.getPathType(), getBot()); + super.addPath(newPath); + + } + @Override + public void mouseDragged(MouseEvent e) { + super.mouseDragged(e); + //ensures that only left click applies the action + if (e.getButton()!=MouseEvent.BUTTON1) + return; + Point2D mousePoint = e.getPoint(); + //moves the path's endpoint while the mouse is being dragged. Also rounds the point if shift is held + if (e.isShiftDown()) + mousePoint = roundTo90(getPath().get(getPath().size()-1).getN1(), mousePoint); + super.getPath().get(getPath().size()-1).setEndLocation(mousePoint); + } + @Override + public void mouseReleased(MouseEvent e) { + super.mouseReleased(e); + //ensures that only left click applies the action + if (e.getButton()!=MouseEvent.BUTTON1) + return; + Point2D mousePoint = e.getPoint(); + if (e.isShiftDown()) + mousePoint = roundTo90(getPath().get(getPath().size()-1).getN1(), mousePoint); + //finalizes the path's location and ensures that the path is not empty + super.getPath().get(getPath().size()-1).setEndLocation(mousePoint); + if (super.getPath().get(getPath().size()-1).isEmpty()) { + super.getPath().remove(getPath().size()-1); + } + } +} diff --git a/src/main/java/MeepMeepDrawing/DrawingScreen.java b/src/main/java/MeepMeepDrawing/DrawingScreen.java new file mode 100644 index 0000000..dc9f5fd --- /dev/null +++ b/src/main/java/MeepMeepDrawing/DrawingScreen.java @@ -0,0 +1,136 @@ +package MeepMeepDrawing; + + +import com.noahbres.meepmeep.MeepMeep; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import javax.imageio.ImageIO; +import javax.swing.JPanel; + +import kotlin.NoWhenBranchMatchedException; + +public class DrawingScreen extends JPanel{ + private static final int ACTION_PANEL_WIDTH = 200; + private static final int CODE_PANEL_HEIGHT = 200; + private BufferedImage background; + + public DrawingScreen(int windowSize) { + setLayout(new BorderLayout()); + setPreferredSize(new Dimension(windowSize+ACTION_PANEL_WIDTH, windowSize)); + try { + background = backgroundToImage(MeepMeep.Background.GRID_BLUE);//Grid blue is the default background in meep meep + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected int getActionPanelWidth() { + return ACTION_PANEL_WIDTH; + } + protected int getCodePanelHeight() { + return CODE_PANEL_HEIGHT; + } + + /** + * Sets the background bufferedimage from a {@link MeepMeep.Background} object + */ + public void setBackground(MeepMeep.Background background) { + try { + this.background = backgroundToImage(background); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2d = (Graphics2D)g; + g2d.drawImage(background,0,0,getWidth()-ACTION_PANEL_WIDTH,getHeight(),null); + Tool.paintPath(g2d); + } + + //TODO: improve this method's access to the Background enum for easier implementation of future field backgrounds + /** + * This method was copied from {@link MeepMeep#setBackground(MeepMeep.Background)}. + * @param background The background enum constant to aquire an image from + * @return A {@link BufferedImage} that contains the image from the background enum contstant + */ + protected BufferedImage backgroundToImage(MeepMeep.Background background) throws IOException { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + BufferedImage backgroundImage; + switch (background.ordinal()) { + case 0: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/misc/field-grid-blue.jpg")); + break; + case 1: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/misc/field-grid-green.jpg")); + break; + case 2: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/misc/field-grid-gray.jpg")); + break; + case 3: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2019-skystone/field-2019-skystone-official.png")); + break; + case 4: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2019-skystone/field-2019-skystone-gf-dark.png")); + break; + case 5: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2019-skystone/field-2019-skystone-innov8rz-light.jpg")); + break; + case 6: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2019-skystone/field-2019-skystone-innov8rz-dark.jpg")); + break; + case 7: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2019-skystone/field-2019-skystone-starwars.png")); + break; + case 8: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2020-ultimategoal/field-2020-innov8rz-dark.jpg")); + break; + case 9: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2021-freightfrenzy/field-2021-official.png")); + break; + case 10: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2021-freightfrenzy/field-2021-adi-dark.png")); + break; + case 11: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2022-powerplay/field-2022-official.png")); + break; + case 12: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2022-powerplay/field-2022-kai-dark.png")); + break; + case 13: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2022-powerplay/field-2022-kai-light.png")); + break; + case 14: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2023-centerstage/field-2023-official.png")); + break; + case 15: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2023-centerstage/field-2023-juice-dark.png")); + break; + case 16: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2023-centerstage/field-2023-juice-light.png")); + break; + case 17: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2024-intothedeep/field-2024-official.png")); + break; + case 18: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2024-intothedeep/field-2024-juice-dark.png")); + break; + case 19: + backgroundImage = ImageIO.read(classLoader.getResourceAsStream("background/season-2024-intothedeep/field-2024-juice-light.png")); + break; + default: + throw new NoWhenBranchMatchedException(); + } + return backgroundImage; + } +} + diff --git a/src/main/java/MeepMeepDrawing/MeepMeepDrawer.java b/src/main/java/MeepMeepDrawing/MeepMeepDrawer.java new file mode 100644 index 0000000..b125c8e --- /dev/null +++ b/src/main/java/MeepMeepDrawing/MeepMeepDrawer.java @@ -0,0 +1,584 @@ +package MeepMeepDrawing; + +import com.acmerobotics.roadrunner.Action; +import com.acmerobotics.roadrunner.SequentialAction; +import com.noahbres.meepmeep.MeepMeep; +import com.noahbres.meepmeep.roadrunner.Constraints; +import com.noahbres.meepmeep.roadrunner.DefaultBotBuilder; +import com.noahbres.meepmeep.roadrunner.entity.RoadRunnerBotEntity; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.util.ArrayList; +import java.util.Stack; + +import javax.swing.AbstractButton; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextPane; +import javax.swing.JToggleButton; +import javax.swing.ScrollPaneConstants; +import javax.swing.Timer; +import javax.swing.text.BadLocationException; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; + +import MeepMeepDrawing.nodeEditing.MoveTool; +import MeepMeepDrawing.nodeEditing.RotateHeadingTool; +import MeepMeepDrawing.nodeEditing.RotateTangentTool; + + +public class MeepMeepDrawer extends MeepMeep implements MouseListener, MouseMotionListener, KeyListener { + private static final int timerHeight = 28; //height of the timer at the bottom of the screen + private static final Color codeBackgroundColor = new Color(50, 50, 50); + private static final Color codeTextColor = new Color(210, 210, 210); + private final DrawingScreen screen; + private final RoadRunnerBotEntity bot; + private final Container meepMeepContentPane; + private final JTextArea codeTextArea = new JTextArea(); + private int fps = 60; + private int windowSize; + private Tool currentTool; //Represents the current tool (e.g., Draw, Move, Rotate) the user is using to interact with the path. + private RobotPath.PathType currentPathType; + private boolean drawingView = true; + private final Stack undoStack = new Stack<>(); + private final Stack redoStack = new Stack<>(); + + + + public MeepMeepDrawer(int windowSize, double botWidth, double botHeight, Constraints constraints) { + super(windowSize); + this.windowSize = windowSize; + //the bot to drive across the screen when previewing the path + bot = createBot(botWidth, botHeight, constraints); + super.addEntity(bot); + + setupListeners(); + screen = new DrawingScreen(windowSize); + setupScreenPanels(); + + meepMeepContentPane = super.getWindowFrame().getContentPane(); + switchToDrawingView(); + setLineDetail((int)((constraints.getMaxVel())/3)); + + setupRepaintLoop(); + super.start(); + } + public MeepMeepDrawer(int windowSize, double botWidth, double botHeight, Constraints constraints, int fps) { + super(windowSize, fps); + this.fps = fps; + bot = createBot(botWidth, botHeight, constraints); + super.addEntity(bot); + + setupListeners(); + + screen = new DrawingScreen(windowSize); + setupScreenPanels(); + + meepMeepContentPane = super.getWindowFrame().getContentPane(); + switchToDrawingView(); + setLineDetail((int)(constraints.getMaxVel()/3)); + + setupRepaintLoop(); + super.start(); + } + + /** + * Creates the simulated bot that drives along the path in the Meep Meep view + */ + private RoadRunnerBotEntity createBot(double botWidth, double botHeight, Constraints constraints) { + return new DefaultBotBuilder(this) + .setDimensions(botWidth, botHeight) + .setConstraints(constraints) + .build(); + } + + /** + * Adds these key, mouse, and mouseMotion listeners to the frame + */ + private void setupListeners() { + super.getWindowFrame().addMouseListener(this); + super.getWindowFrame().addMouseMotionListener(this); + super.getWindowFrame().addKeyListener(this); + super.getWindowFrame().setFocusable(true); + } + + /** + * Sets the background of both the meep meep view and the drawing view according to a {@link Background} object + */ + public void setDrawingBackground(Background background) { + screen.setBackground(background); + super.setBackground(background); + } + + /** + * Controls how detailed the line previewing the robots path is. Specifically, this controls how frequently a dot + * is drawn at a point as the path is being interpolated. High values may cause lag, and lower values will often reduce lag + * @param lineDetail the detail to set to + */ + public void setLineDetail(int lineDetail) { + RobotPath.setLineDetail(lineDetail); + } + + /** + * Starts a Timer that will periodically repaint the drawing screen and update the code view, + * ensuring that the user interface stays up-to-date while the simulation is running. + */ + private void setupRepaintLoop() { + new Timer(1000 / fps, actionEvent -> { + if (!drawingView) + return; + syncCodeAndPath(); + screen.repaint(); + }).start(); + } + + /** + * Updates the code text area or the drawing view depending on whether the user is editing + * the code or drawing a path. If the user is not editing the code, it syncs the text area with + * the current path. If the user is editing the code, it updates the path accordingly. + */ + private void syncCodeAndPath() { + String actionString = currentTool.getActionString(); + + //checks if the user is drawing on a path and that the path has changed + if (!codeTextArea.hasFocus() && !codeTextArea.getText().equals(actionString)) { + addToHistory(codeTextArea.getText()); + codeTextArea.setText(actionString); + } + //otherwise checks if the user is editing the code + else if (codeTextArea.hasFocus()) { + currentTool.tryParseCodePath(codeTextArea.getText()); + } + } + + /** + * Adds the current action string to the undo stack, ensuring no duplicate actions are pushed. + * It also clears the redo stack when a new action is added. + * @param actionString the string representation of the robot's path to store + */ + private void addToHistory(String actionString) { + //if the undo stack is empty, there is no need to worry about duplicates + if (undoStack.isEmpty()) { + undoStack.push(actionString); + redoStack.clear(); + return; + } + //otherwise the undo stack is not empty, and duplicates should not be pushed + if (!undoStack.peek().equals(actionString)) { + undoStack.push(actionString); + redoStack.clear(); + } + } + + + + + /** + * Sets up both the action panel and the code panel on the right and bottom of the screen respectively + */ + private void setupScreenPanels() { + JPanel actionPanel = setupActionPanel(); + + //sets up button panel + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS)); + buttonPanel.setBackground(actionPanel.getBackground()); + setupAllRadioButtons(buttonPanel); + setupCodePanel(buttonPanel); + actionPanel.add(buttonPanel); + + setupInstructionPanel(actionPanel, buttonPanel); + } + + /** + * Adds text at the bottom of the action panel that tells the user how to operate the program + */ + private void setupInstructionPanel(JPanel actionPanel, JPanel buttonPanel) { + //setting up instruction pnale + JPanel instructionPanel = new JPanel(); + instructionPanel.setBackground(actionPanel.getBackground()); + //setting up text area + JTextPane instructions = setupTextPane(actionPanel); + //formatting the text + SimpleAttributeSet style = new SimpleAttributeSet(); + //sets the first line indent to -15 from the left indent of 20. This makes a hanging indent + StyleConstants.setFirstLineIndent(style, -15); + StyleConstants.setLeftIndent(style, 20); + StyleConstants.setLineSpacing(style, 0.1f); + //applys the formatting to the pane + instructions.getStyledDocument().setParagraphAttributes(0,0,style,false); + try { + instructions.getStyledDocument().insertString(0,getInstructionString(), style); + } catch (BadLocationException e) { + e.printStackTrace(); + } + JScrollPane scrollPane = setupInstructionScrollPane(buttonPanel, instructions); + instructionPanel.add(scrollPane); + actionPanel.add(instructionPanel); + } + + private JScrollPane setupInstructionScrollPane(JPanel buttonPanel, JTextPane instructions) { + JScrollPane scrollPane = new JScrollPane(instructions); + scrollPane.setBackground(instructions.getBackground()); + scrollPane.setBorder(null); + //wraps the instructions in a scroll pane so the text is readable at any window size + scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + //sets the bounds to whatever space is left on the side panel after the button panel has been added + scrollPane.setPreferredSize(new Dimension(screen.getActionPanelWidth(), screen.getPreferredSize().height- buttonPanel.getPreferredSize().height)); + return scrollPane; + } + + /** + * Sets up the pane used to hold instructions for the user to understand how to use the program + * @param actionPanel the side panel on the right of the screen + * @return the setup pane ready for text and formatting + */ + private JTextPane setupTextPane(JPanel actionPanel) { + JTextPane instructions = new JTextPane(); + instructions.setBackground(actionPanel.getBackground()); + //setting default paramaters + instructions.setForeground(Color.BLACK); + instructions.setEditable(false); + instructions.setFont(new Font(Font.SERIF, Font.PLAIN, 15)); + instructions.setFocusable(false); + return instructions; + } + + /** + * Generates & returns the string that describes how to use the program + * @return The string the user can read to understand how to use the program + */ + private String getInstructionString() { + return ("Draw: Click and drag to draw points\n" + + "Move: Click and drag a point to move it\n" + + "Rotate: Click and drag a node to change its heading or tangent\n" + + "Select: Click on a path to view its type. Select a path type to change its type\n" + + "Draw & Rotate: Hold shift to snap to 90 degrees\n" + + "All: Right click the last point in a path to change its type\n" + + "All: Press 'Toggle Code' to view, edit, copy, and paste the associated java code. Press escape to stop editing\n" + + "All: Press spacebar to switch between running the code and editing it\n" + + "All: Press cmd/ctrl z to undo, cmd/ctrl shift z to redo"); + } + + /** + * Sets up the action panel on the side of the screen used to control path type and editing type + * @return The action panel its created + */ + private JPanel setupActionPanel() { + JPanel actionPanel = new JPanel(); + actionPanel.setBackground(Color.LIGHT_GRAY); + actionPanel.setPreferredSize(new Dimension(screen.getActionPanelWidth(), screen.getHeight()-screen.getCodePanelHeight())); + actionPanel.setLayout(new FlowLayout()); + screen.add(actionPanel, BorderLayout.EAST); + return actionPanel; + } + + + /** + * Sets up the code panel at the bottom of the screen for the user to edit and view + * @param actionPanel The side panel of the screen to add a button to + */ + private void setupCodePanel(JPanel actionPanel) { + //setting up the panel that holds the code + JPanel codePanel = new JPanel(); + codePanel.setPreferredSize(new Dimension(screen.getWidth(), screen.getCodePanelHeight())); + codePanel.setBackground(codeBackgroundColor); + codePanel.setLayout(new BorderLayout()); + + //setting up the text area that the user can write in + codeTextArea.setBackground(codePanel.getBackground()); + codeTextArea.setForeground(codeTextColor); + codeTextArea.setCaretColor(codeTextColor); + //adds a keylistener that listens for the escape key that removes focus from the text area to signify that the user is done editing + codeTextArea.addKeyListener(new KeyListener() { + @Override + public void keyTyped(KeyEvent keyEvent) {} + @Override + public void keyPressed(KeyEvent keyEvent) {} + @Override + public void keyReleased(KeyEvent keyEvent) { + if (keyEvent.getKeyCode()==KeyEvent.VK_ESCAPE) { + getWindowFrame().requestFocus(); + } + } + }); + + //using a scroll pane so that the user can scroll to view more lines of code + JScrollPane scrollPane = new JScrollPane(codeTextArea); + scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS); + + //adds the panels to the screen + screen.add(codePanel, BorderLayout.SOUTH); + codePanel.add(scrollPane, BorderLayout.CENTER); + codePanel.setVisible(false); + + //adds a button to toggle visibility of the code + JToggleButton toggleCode = new JToggleButton("Toggle Code"); + toggleCode.addActionListener(actionEvent -> codePanel.setVisible(!codePanel.isVisible())); + toggleCode.setFocusable(false); + actionPanel.add(toggleCode); + } + + + /** + * Adds all the operation-selecting buttons to a panel + * @param panel The panel to add the buttons to + */ + private void setupAllRadioButtons(JPanel panel) { + setupEditorModeRadioButtons(panel); + setupPathRadioButtons(panel); + } + + /** + * Adds the path selecting buttons (spline, strafe, etc) to a panel + */ + private void setupPathRadioButtons(JPanel panel) { + ButtonGroup pathTypes = new ButtonGroup(); + panel.add(Box.createVerticalStrut(15)); + panel.add(new JLabel("\t" + "Path Type:")); + //adds a new button for each value in the PathType enum + for (int i = 0; i < RobotPath.PathType.values().length; i++) { + JRadioButton button = new JRadioButton(RobotPath.PathType.values()[i].name()); + //sets focusable to false so that focus is not taken away from frame + button.setFocusable(false); + pathTypes.add(button); + panel.add(button); + addPathTypeActionTo(button); + //sets the first button as selected by default + if (i==0) { + pathTypes.setSelected(button.getModel(), true); + button.doClick(); + } + } + } + + /** + * Adds the edit tool selection buttons (draw, move, rotate, etc.) to a panel + */ + private void setupEditorModeRadioButtons(JPanel panel) { + ButtonGroup editorModes = new ButtonGroup(); + panel.add(Box.createVerticalStrut(15)); + panel.add(new JLabel("\t" + "Editor Mode:")); + for (int i = 0; i < Tool.EditorMode.values().length; i++) { + JRadioButton button = new JRadioButton(Tool.EditorMode.values()[i].name()); + //sets focusable to false so that focus is not taken away from frame + button.setFocusable(false); + editorModes.add(button); + panel.add(button); + addEditorActionTo(button); + //sets the first button as selected by default + if (i==0) { + editorModes.setSelected(button.getModel(), true); + button.doClick(); + } + } + } + + /** + * Assigns a button to its corresponding editing action enum param (draw, move, rotate, etc.) and action + */ + private void addEditorActionTo(JRadioButton button) { + switch (Tool.EditorMode.valueOf(button.getText())) { + case Draw: + button.addActionListener(actionEvent -> currentTool = new DrawTool(currentPathType, bot)); + break; + case Move: + button.addActionListener(actionEvent -> currentTool = new MoveTool(currentPathType, bot)); + break; + case Select: + button.addActionListener(actionEvent -> currentTool = new SelectTool(currentPathType, bot)); + break; + case Rotate_Path: + button.addActionListener(actionEvent -> currentTool = new RotateTangentTool(currentPathType, bot)); + break; + case Rotate_Heading: + button.addActionListener(actionEvent -> currentTool = new RotateHeadingTool(currentPathType, bot)); + break; + } + } + + /** + * Assigns a button to its corresponding path type enum param (spline, strafe, etc.) and action + */ + private void addPathTypeActionTo(AbstractButton button) { + button.addActionListener(actionEvent -> { + currentPathType = RobotPath.PathType.valueOf(button.getText()); + currentTool.setCurrentPathType(currentPathType); + }); + } + + + /** + * Clears any existing actions before starting new ones + */ + private void clearExistingActions() { + if (bot.getCurrentActionTimeline() != null) { + bot.getCurrentActionTimeline().getEvents().clear(); + } + } + + /** + * Starts simulating the bot's movement along the designed path + */ + private void startBotPath() { + bot.pause(); + super.removeEntity(bot); + super.start(); + ArrayList actions = new ArrayList<>(); + currentTool.getPath().forEach(robotPath -> actions.add(robotPath.getActionInInches())); + bot.runAction(new SequentialAction(actions)); + super.addEntity(bot); + bot.unpause(); + } + + + /** + * Switches the window to the view that the user can draw and edit paths in + */ + private void switchToDrawingView() { + super.getWindowFrame().setContentPane(screen); + super.getWindowFrame().setSize(screen.getPreferredSize().width, meepMeepContentPane.getPreferredSize().height+timerHeight); + } + /** + * Switches to the Roadrunner view and runs the path created by the user + */ + private void switchToRoadrunnerView() { + clearExistingActions(); + startBotPath(); + super.getWindowFrame().setContentPane(meepMeepContentPane); + super.getWindowFrame().setSize(meepMeepContentPane.getWidth(), meepMeepContentPane.getPreferredSize().height+timerHeight); + super.start(); + } + + /** + * Toggles between the drawing view (where the user edits paths) and the Roadrunner simulation view + * (where the bot runs the created path). + */ + private void toggleView() { + drawingView = !drawingView; + if (drawingView) + switchToDrawingView(); + else + switchToRoadrunnerView(); + } + + /** + * Undoes the most recent action by popping the undo stack and updating the current tool's path and the code area. + * The undone action is pushed to the redo stack, allowing it to be redone later. + */ + private void undo() { + if (undoStack.isEmpty()) + return; + // Saves the current state for potential redo + redoStack.push(currentTool.getActionString()); + // Restores and syncs the previous action from the undo stack + currentTool.tryParseCodePath(undoStack.pop()); + codeTextArea.setText(currentTool.getActionString()); + } + + + /** + * Redoes the most recent undone action by popping the redo stack and updating the current tool's path and the code area. + * The redone action is moved to the undo stack. + */ + private void redo() { + if (redoStack.isEmpty()) + return; + // Saves the current state for potential undo + undoStack.push(currentTool.getActionString()); + // Restores and syncs the previous action from the redo stack + currentTool.tryParseCodePath(redoStack.pop()); + codeTextArea.setText(currentTool.getActionString()); + } + + @Override + public void keyTyped(KeyEvent e) {} + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode()==KeyEvent.VK_SPACE) { + toggleView(); + } + if (e.getKeyCode()==KeyEvent.VK_Z && (e.isMetaDown() || e.isControlDown())) { + if (e.isShiftDown()) + redo(); + else + undo(); + } + } + @Override + public void keyReleased(KeyEvent e) {} + + @Override + public void mouseClicked(MouseEvent e) { + if (outsideBounds(e.getPoint())) + return; + e.translatePoint(0,-timerHeight); + currentTool.mouseClicked(e); + } + @Override + public void mousePressed(MouseEvent e) { + super.getWindowFrame().requestFocus(); + if (outsideBounds(e.getPoint())) + return; + e.translatePoint(0,-timerHeight); + currentTool.mousePressed(e); + } + @Override + public void mouseReleased(MouseEvent e) { + if (outsideBounds(e.getPoint())) + return; + e.translatePoint(0,-timerHeight); + currentTool.mouseReleased(e); + } + @Override + public void mouseEntered(MouseEvent e) { + if (outsideBounds(e.getPoint())) + return; + e.translatePoint(0,-timerHeight); + currentTool.mouseEntered(e); + } + @Override + public void mouseExited(MouseEvent e) { + if (outsideBounds(e.getPoint())) + return; + e.translatePoint(0,-timerHeight); + currentTool.mouseExited(e); + } + @Override + public void mouseDragged(MouseEvent e) { + if (outsideBounds(e.getPoint())) + return; + e.translatePoint(0,-timerHeight); + currentTool.mouseDragged(e); + } + @Override + public void mouseMoved(MouseEvent e) { + if (outsideBounds(e.getPoint())) + return; + e.translatePoint(0,-timerHeight); + currentTool.mouseMoved(e); + } + private boolean outsideBounds(Point p) { + Rectangle bounds = new Rectangle(windowSize, windowSize); + return !bounds.contains(p); + } +} diff --git a/src/main/java/MeepMeepDrawing/Node.java b/src/main/java/MeepMeepDrawing/Node.java new file mode 100644 index 0000000..63b3113 --- /dev/null +++ b/src/main/java/MeepMeepDrawing/Node.java @@ -0,0 +1,141 @@ +package MeepMeepDrawing; +import com.acmerobotics.roadrunner.Pose2d; +import com.acmerobotics.roadrunner.Vector2d; +import com.noahbres.meepmeep.core.util.FieldUtil; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Polygon; +import java.awt.geom.AffineTransform; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; + +public class Node extends Point2D.Double { + private double heading = 0; + private double tangent = 0; + private static final int NODE_RADIUS = 10; + private static final Color NODE_COLOR = new Color(36, 86, 224); + private static final Color HEADING_ARROW_COLOR = new Color(55, 255, 0, 150); + private static final Color TANGENT_ARROW_COLOR = new Color(255, 0, 0, 150); + + /** + * Creates a node according to the raw x and y values passed in + */ + public Node(double x, double y, double heading, double tangent) { + super(x,y); + this.heading = heading; + this.tangent = tangent; + } + + /** + * Creates a node according to the raw values of the passed in point + */ + public Node(Point p, double heading, double tangent) { + super(p.x, p.y); + this.heading = heading; + this.tangent = tangent; + } + /** + * Creates a Node according to a vector object scaled from inches to pixels + */ + public Node(Vector2d v, double heading) { + super(v.x,v.y); + Node n = scaleToPixels(); + this.x = n.x; + this.y = n.y; + this.heading = heading; + this.tangent = heading; + } + /** + * Creates a Node according to a pose object scaled from inches to pixels + */ + public Node(Pose2d p, double tangent) { + super(p.position.x,p.position.y); + Node n = scaleToPixels(); + this.x = n.x; + this.y = n.y; + this.heading = p.heading.toDouble(); + this.tangent = tangent; + } + + /** + * Draws the node and the arrow(s) representing the heading and/or tangent + */ + public void paint(Graphics2D g2d) { + g2d.setColor(NODE_COLOR); + int radius = NODE_RADIUS; + g2d.fillOval((int)x-radius/2, (int)y-radius/2, radius, radius); + drawArrow(g2d, heading, HEADING_ARROW_COLOR); + drawArrow(g2d, tangent, TANGENT_ARROW_COLOR); + } + + /** + * Draws an arrow in a certain direction with a certainn color with a certain graphics object + * @param g2d The graphics object to paint with + * @param direction The direction of the angle in radians + * @param color The color of the arrow + */ + private void drawArrow(Graphics2D g2d, double direction, Color color) { + g2d.setStroke(new BasicStroke(5)); + g2d.setColor(color); + Line2D arrow = new Line2D.Double(this, new Double(x+20, y)); + int x = (int)this.x; + int y = (int)this.y; + Polygon triangle = new Polygon(new int[]{x+20,x+25,x+20}, new int[]{y-6, y, y+6}, 3); + AffineTransform graphicsTransform = g2d.getTransform(); + AffineTransform rotateInstance = new AffineTransform(); + rotateInstance.translate(x, y); + rotateInstance.rotate(direction); + rotateInstance.translate(-x, -y); + g2d.transform(rotateInstance); + g2d.draw(arrow); + g2d.fill(triangle); + g2d.setTransform(graphicsTransform); + } + + protected Pose2d getHeadingPose() { + return new Pose2d(x,y,heading); + } + protected Vector2d getVector() { + return new Vector2d(x,y); + } + protected double getHeading() { + return heading; + } + protected double getTangent() { + return tangent; + } + /** + * @return A node where all of its locational data has been scaled from pixels to inches + */ + protected Node scaleToInches() { + Vector2d inchPoint = FieldUtil.screenCoordsToFieldCoords(new Vector2d(x,y)); + return new Node(inchPoint.x, inchPoint.y, -heading, -tangent); + } + + /** + * @return A node where all of its locational data has been scaled from inches to pixels + */ + protected Node scaleToPixels() { + Vector2d pixelPoint = FieldUtil.fieldCoordsToScreenCoords(new Vector2d(x,y)); + return new Node(pixelPoint.x, pixelPoint.y, -heading, -tangent); + } + protected void setHeading(double heading) { + this.heading = heading; + } + protected void setTangent(double tangent) { + this.tangent = tangent; + } + + /** + * @return A pose of this position scaled to inches + */ + protected Pose2d toPoseInInches() { + return scaleToInches().getHeadingPose(); + } + + + +} diff --git a/src/main/java/MeepMeepDrawing/RobotPath.java b/src/main/java/MeepMeepDrawing/RobotPath.java new file mode 100644 index 0000000..4df587e --- /dev/null +++ b/src/main/java/MeepMeepDrawing/RobotPath.java @@ -0,0 +1,276 @@ +package MeepMeepDrawing; + +import com.acmerobotics.roadrunner.Action; +import com.acmerobotics.roadrunner.Pose2d; +import com.acmerobotics.roadrunner.TrajectoryActionBuilder; +import com.noahbres.meepmeep.roadrunner.entity.ActionEvent; +import com.noahbres.meepmeep.roadrunner.entity.ActionTimeline; +import com.noahbres.meepmeep.roadrunner.entity.RoadRunnerBotEntity; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.geom.Point2D; +import java.util.ArrayList; + +public class RobotPath { + public enum PathType { + strafeToLinearHeading, + strafeToConstantHeading, + splineTo, + splineToConstantHeading, + splineToLinearHeading, + + + } + private static double LINE_DETAIL = 40; + private final Node n1; + private final Node n2; + private final RoadRunnerBotEntity bot; + private PathType type; + private boolean highlight = false; + private static final Color NODE_HIGHLIGHT_COLOR = new Color(32, 139, 205, 20); + private static final Color NODE_NORMAL_COLOR = new Color(32, 139, 205, 10); + private static final int OUTER_CIRCLE_PAINT_RADIUS = 10; + private static final int INNER_CIRCLE_PAINT_RADIUS = 3; + + + + public RobotPath (Node n1, Node n2, PathType type, RoadRunnerBotEntity bot) { + this.n1 = n1; + this.n2 = n2; + this.bot = bot; + this.type = type; + } + + public Node getN1() { + return n1; + } + public Node getN2() { + return n2; + } + public PathType getPathType() { + return type; + } + public void setEndLocation(Point2D p) { + this.n2.setLocation(p); + } + + /** + * Controls how detailed the line previewing the robots path is. Specifically, this controls how frequently a dot + * is drawn at a point as the path is being interpolated + * @param lineDetail the detail to set to + */ + protected static void setLineDetail(int lineDetail) { + LINE_DETAIL = lineDetail; + } + + protected boolean isEmpty() { + return n1.distance(n2)<1; + } + + protected void paint(Graphics2D g2d) { + if (n1==null || n2==null) + return; + n1.paint(g2d); + n2.paint(g2d); + previewAction(g2d, getAction(n1, n2)); + //bolds the path and draws a string in the center of the path with the type of the path + if (highlight) { + Point midPoint = new Point((int)((n1.x+n2.x)/2), (int)((n1.y+n2.y)/2)); + g2d.setColor(Color.WHITE); + String name = type.name(); + int width = g2d.getFontMetrics().stringWidth(name); + g2d.drawString(name, midPoint.x-width/2, midPoint.y); + } + + } + + /** + * Makes an Action object with the two passed in nodes that can be ran on a RoadRunnerBotEntity + * @param n1 The starting node of the path + * @param n2 The ending node of the path + * @return An action to be ran on a RoadRunnerBotEntity + */ + private Action getAction(Node n1, Node n2) { + if (n1.distance(n2)<1) {//roadrunner throws an error if the start and end point are the same + return null; + } + validateAngles(); + TrajectoryActionBuilder builder; + //calls the corresponding robot action according to the path type of the RobotPath + switch (type) { + case strafeToLinearHeading: + builder = bot.getDrive().actionBuilder(n1.getHeadingPose()); + return builder.strafeToLinearHeading(n2.getVector(), n2.getHeading()).build(); + case strafeToConstantHeading: + builder = bot.getDrive().actionBuilder(n1.getHeadingPose()); + return builder.strafeToConstantHeading(n2.getVector()).build(); + case splineTo: + builder = bot.getDrive().actionBuilder(n1.getHeadingPose()); + return builder.splineTo(n2.getVector(), n2.getHeading()).build(); + case splineToConstantHeading: + builder = bot.getDrive().actionBuilder(n1.getHeadingPose()); + return builder.splineToConstantHeading(n2.getVector(), n2.getTangent()).build(); + case splineToLinearHeading: + builder = bot.getDrive().actionBuilder(n1.getHeadingPose()); + return builder.splineToLinearHeading(n2.getHeadingPose(), n2.getTangent()).build(); + default: + return null; + } + } + + /** + * Makes an Action object represented in inches with the two passed in nodes that can be ran on a RoadRunnerBotEntity + * @return an action to be ran on a RoadRunnerBotEntity + */ + protected Action getActionInInches() { + return getAction(n1.scaleToInches(), n2.scaleToInches()); + } + + /** + * Draws the path of an action onto a graphics object + * @param g2d The graphics object to use to paint + * @param action The action to paint + */ + private void previewAction(Graphics2D g2d, Action action) { + if (action==null) { + return; + } + if (bot.getCurrentActionTimeline() != null) + bot.getCurrentActionTimeline().getEvents().clear(); + + bot.runAction(action); + paintBotPath(g2d); + } + + /** + * Draws each point along the bots path as it runs whatever action it is currently doing + * @param g2d The graphics object to use to paint + */ + private void paintBotPath(Graphics2D g2d) { + ArrayList points = getPointsAlongPath(); + if (points==null) + return; + for (Point p : points) { + if (highlight) + g2d.setColor(NODE_HIGHLIGHT_COLOR); + else + g2d.setColor(NODE_NORMAL_COLOR); + //draws two circles, one bigger and more transparent one and one smaller and more opaque one + g2d.fillOval(p.x - OUTER_CIRCLE_PAINT_RADIUS, p.y - OUTER_CIRCLE_PAINT_RADIUS, OUTER_CIRCLE_PAINT_RADIUS*2, OUTER_CIRCLE_PAINT_RADIUS*2); + g2d.setColor(NODE_HIGHLIGHT_COLOR); + g2d.fillOval(p.x - INNER_CIRCLE_PAINT_RADIUS, p.y - INNER_CIRCLE_PAINT_RADIUS, INNER_CIRCLE_PAINT_RADIUS*2, INNER_CIRCLE_PAINT_RADIUS*2); + } + } + protected void setPathType(PathType type) { + this.type = type; + } + + /** + * Generates a list of all the points along the path that the simulated robot is currently running + * @return The list of every point along the robot's path + */ + protected ArrayList getPointsAlongPath() { + ActionTimeline timeline = bot.getCurrentActionTimeline(); + if (timeline == null) + return null; + ArrayList points = new ArrayList<>(); + //goes through every action in the bot's path + for (ActionEvent e : timeline.getEvents()) { + //goes through every point in the timeline and adds the point to the list + for (double t = 0; t < timeline.getDuration(); t += 1.0/LINE_DETAIL) { + Pose2d pose = e.getA().get(t); + points.add(new Point((int) pose.position.x, (int) pose.position.y)); + } + } + return points; + } + + /** + * Runs this path on the simulated bot + */ + protected void runPath() { + if (bot.getCurrentActionTimeline() != null) + bot.getCurrentActionTimeline().getEvents().clear(); + Action a = getAction(n1,n2); + if (a==null) + return; + bot.runAction(a); + } + protected void setHighlight(boolean highlight) { + this.highlight = highlight; + } + + /** + * Sets the path angle according to the current path type + * @param angle The angle to set to in radians + */ + public void setPathAngle(Node n, double angle) { + //in some robotpaths, the path angle + switch (type) { + case strafeToLinearHeading: + n.setHeading(angle); + n.setTangent(angle); + break; + case strafeToConstantHeading: + n.setHeading(angle); + n.setTangent(angle); + break; + case splineTo: + n.setTangent(angle); + n.setHeading(angle); + break; + case splineToLinearHeading: + n.setTangent(angle); + break; + case splineToConstantHeading: + n.setTangent(angle); + break; + } + } + + /** + * Sets the heading angle according to the current path type + * @param angle the angle to set the heading to in radians + */ + public void setHeadingAngle(Node n, double angle) { + switch (type) { + case strafeToLinearHeading: + n.setHeading(angle); + n.setTangent(angle); + break; + case strafeToConstantHeading: + n.setHeading(angle); + n.setTangent(angle); + break; + case splineTo: + n.setTangent(angle); + n.setHeading(angle); + break; + case splineToLinearHeading: + n.setHeading(angle); + break; + case splineToConstantHeading: + n.setHeading(angle); + break; + } + } + + /** + * Fixes any discrepensies between path and heading angles that would cause the robot to instantly rotate to a different pose + */ + protected void validateAngles() { + if (n2.getHeading()!=n1.getHeading()) { + if (type==PathType.splineToConstantHeading) { + n2.setHeading(n1.getHeading()); + } + if (type==PathType.strafeToConstantHeading) { + n2.setHeading(n1.getHeading()); + n2.setTangent(n1.getTangent()); + } + } + } + + +} diff --git a/src/main/java/MeepMeepDrawing/SelectTool.java b/src/main/java/MeepMeepDrawing/SelectTool.java new file mode 100644 index 0000000..7dd835e --- /dev/null +++ b/src/main/java/MeepMeepDrawing/SelectTool.java @@ -0,0 +1,61 @@ +package MeepMeepDrawing; + +import com.noahbres.meepmeep.roadrunner.entity.RoadRunnerBotEntity; + +import java.awt.Point; +import java.awt.event.MouseEvent; + +public class SelectTool extends Tool{ + + private RobotPath selectedPath = null; + public SelectTool(RobotPath.PathType pathType, RoadRunnerBotEntity bot) { + super(pathType, bot); + } + + @Override + public void mouseClicked(MouseEvent e) { + super.mouseClicked(e); + //resets the previous path's highlight + if (selectedPath!=null) + selectedPath.setHighlight(false); + + //highlights the clicked path + selectedPath = getPathAt(e.getPoint()); + if (selectedPath!=null) + selectedPath.setHighlight(true); + + } + + /** + * Allows the user to change the path type of the selected path + * @param type the path type to change to + */ + @Override + public void setCurrentPathType(RobotPath.PathType type) { + super.setCurrentPathType(type); + if (selectedPath==null) + return; + selectedPath.setPathType(type); + selectedPath.validateAngles(); + } + + + /** + * Returns the path overlapping with a point + * @param p The point to check for a path at + * @return A path overlapping the point, or null if there is no path + */ + public RobotPath getPathAt(Point p) { + for (RobotPath path : getPath()) { + path.runPath(); + for (Point pathPoint : path.getPointsAlongPath()) { + if (pathPoint.distance(p) < getMouseToNodeSensitivity()) + return path; + } + } + return null; + } + + + +} diff --git a/src/main/java/MeepMeepDrawing/Tool.java b/src/main/java/MeepMeepDrawing/Tool.java new file mode 100644 index 0000000..bbb57b6 --- /dev/null +++ b/src/main/java/MeepMeepDrawing/Tool.java @@ -0,0 +1,328 @@ +package MeepMeepDrawing; + +import com.acmerobotics.roadrunner.Pose2d; +import com.acmerobotics.roadrunner.Vector2d; +import com.noahbres.meepmeep.roadrunner.entity.RoadRunnerBotEntity; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.geom.Point2D; +import java.util.ArrayList; + +public abstract class Tool implements MouseListener, MouseMotionListener { + + public enum EditorMode{ + Draw, + Move, + Rotate_Heading, + Rotate_Path, + Select + } + private static final int MOUSE_TO_NODE_SENSITIVITY = 20; + private static final ArrayList path = new ArrayList<>(); + private RobotPath.PathType currentPathType; + private final RoadRunnerBotEntity bot; + + public Tool(RobotPath.PathType pathType, RoadRunnerBotEntity bot) { + this.currentPathType = pathType; + this.bot = bot; + path.forEach(robotPath-> {robotPath.setHighlight(false);}); + } + + @Override + public void mouseMoved(MouseEvent e) {} + @Override + public void mousePressed(MouseEvent e) {} + @Override + public void mouseDragged(MouseEvent e) {} + @Override + public void mouseReleased(MouseEvent e) {} + @Override + public void mouseClicked(MouseEvent e) { + //right click removes the very last node in the path + if (e.getButton()==MouseEvent.BUTTON3) { + if (path.get(path.size()-1).getN2().distance(e.getPoint()) getPath() { + return path; + } + protected void addPath(RobotPath robotPath) { + path.add(robotPath); + } + protected RobotPath.PathType getPathType() { + return currentPathType; + } + protected RoadRunnerBotEntity getBot() { + return bot; + } + + /** + * Starts the path at a certain point when no other paths have been drawn + * @param p1 The very first point in a path + */ + protected void initPath(Point p1) { + path.add(new RobotPath(new Node(p1, 0, 0), new Node(p1,0,0), currentPathType, bot)); + } + protected static void paintPath(Graphics2D g2d) { + path.forEach(path -> path.paint(g2d)); + } + protected int getMouseToNodeSensitivity() { + return MOUSE_TO_NODE_SENSITIVITY; + } + + /** + * Makes a point that has been rounded from the point to round to the nearest 90 degree angle around the startpoint + * @param startPoint The point to round according to + * @param pointToRound The point to round to the nearest 90 degrees + * @return The rounded point + */ + protected Point2D roundTo90(Point2D startPoint, Point2D pointToRound) { + double xDiff = Math.abs(pointToRound.getX()-startPoint.getX()); + double yDiff = Math.abs(pointToRound.getY()-startPoint.getY()); + if (xDiff>yDiff) { + return new Point2D.Double(pointToRound.getX(), startPoint.getY()); + } + else { + return new Point2D.Double(startPoint.getX(), pointToRound.getY()); + } + } + + /** + * Generates a string that contains the code for the current path on the screen + * @return The string generated that contains the code for the user to copy or edit + */ + protected String getActionString() { + if (path.isEmpty()) + return ""; + //initiates the string at the starting pose of the first path in the list + StringBuilder actionString = new StringBuilder("bot.getDrive().actionBuilder(" + poseToString(path.get(0).getN1().toPoseInInches()) + ")"); + //adds all actions to the string + for (int i = 0; i paths; + //trys to interpret the lines of code the user has written. if the code is incomprehensible to the program, nothing in the path is updated + try { + paths = interpretPaths(lines); + } catch (IndexOutOfBoundsException | NumberFormatException e) { + return; + } + Tool.path.clear(); + Tool.path.addAll(paths); + } + + /** + * Attempts to interpret the lines of code the user has written and turn it into a list of robotpaths. + * If a piece of code is encountered that does not match known syntax, an error is thrown + * @param lines The lines of code to interpret + * @return A list of the paths according to the code + */ + private ArrayList interpretPaths(String[] lines) throws IndexOutOfBoundsException, NumberFormatException { + Pose2d startingPose = interpretPose2d(lines[0]); + ArrayList paths = new ArrayList<>(); + //starts i at 1 because the first line of code merely sets the starting pose, it doesn't actually define a path + for (int i = 1; i < lines.length; i++) { + Node previousNode; + //sets the previous node to the previous path's ending node, or the starting node if there is no previous node + if (paths.isEmpty()) + previousNode = new Node(startingPose, startingPose.heading.toDouble()); + else + previousNode = paths.get(paths.size()-1).getN2(); + + //interprets the correct path type depending on the syntax + if (lines[i].contains(RobotPath.PathType.strafeToLinearHeading.name())) + paths.add(interpretStrafeLinearHeading(lines[i], previousNode)); + else if (lines[i].contains(RobotPath.PathType.strafeToConstantHeading.name())) + paths.add(interpretStrafeConstantHeading(lines[i], previousNode)); + else if (lines[i].contains(RobotPath.PathType.splineToConstantHeading.name())) + paths.add(interpretSplineConstantHeading(lines[i], previousNode)); + else if (lines[i].contains(RobotPath.PathType.splineToLinearHeading.name())) + paths.add(interpretSplineLinearHeading(lines[i], previousNode)); + else if (lines[i].contains(RobotPath.PathType.splineTo.name())) + paths.add(interpretSpline(lines[i], previousNode)); + } + return paths; + } + + + //the following methods interpret each different type of path that is used in the program and returns a corresponding RobotPath of the same type + private RobotPath interpretStrafeLinearHeading(String line, Node previousNode) { + return new RobotPath(previousNode, new Node(interpretVector2d(line), interpretTangent(line)), RobotPath.PathType.strafeToLinearHeading, bot); + } + private RobotPath interpretStrafeConstantHeading(String line, Node previousNode) { + return new RobotPath(previousNode, new Node(interpretVector2d(line), previousNode.getHeading()), RobotPath.PathType.strafeToConstantHeading, bot); + } + private RobotPath interpretSpline(String line, Node previousNode) { + return new RobotPath(previousNode, new Node(interpretVector2d(line), interpretTangent(line)), RobotPath.PathType.splineTo, bot); + } + private RobotPath interpretSplineConstantHeading(String line, Node previousNode) { + + return new RobotPath(previousNode, new Node(interpretVector2d(line), interpretTangent(line)), RobotPath.PathType.splineToConstantHeading, bot); + } + private RobotPath interpretSplineLinearHeading(String line, Node previousNode) { + return new RobotPath(previousNode, new Node(interpretPose2d(line), interpretTangent(line)), RobotPath.PathType.splineToLinearHeading, bot); + } + + /** + * Turns a string containing a Vector2d definition into a Vector2d + * @param line The line of code to look for a vector definition + * @return A vector object interpreted from the line of code + */ + private Vector2d interpretVector2d(String line) { + String vector = "newVector2d("; + int index = line.indexOf(vector)+vector.length(); + StringBuilder x = new StringBuilder(); + //adds each digit within the string until it reaches a ',' signifying the end of the number + while (line.charAt(index)!=',') { + x.append(line.charAt(index)); + index++; + } + StringBuilder y = new StringBuilder(); + //index will be at the ',' character, so 1 is added to it to go to the next number + index++; + //adds each digit within the string until it reaches a ')' signifying the end of the vector + while (line.charAt(index)!=')') { + y.append(line.charAt(index)); + index++; + } + return new Vector2d(Double.parseDouble(x.toString()), Double.parseDouble(y.toString())); + } + + /** + * Turns a string containing a Pose2d definition into a Pose2d + * @param line The line of code to look for a pose definition + * @return A pose object interpreted from the line of code + */ + private Pose2d interpretPose2d(String line) { + String pose = "newPose2d("; + int index = line.indexOf(pose)+pose.length(); + StringBuilder x = new StringBuilder(); + //adds each digit of the x pos until a ',' is reached + while (line.charAt(index)!=',') { + x.append(line.charAt(index)); + index++; + } + //1 is added to get put the index on a number instead of a ',' + index++; + StringBuilder y = new StringBuilder(); + //adds each digit of y + while (line.charAt(index)!=',') { + y.append(line.charAt(index)); + index++; + } + StringBuilder heading = new StringBuilder(); + index++; + //adds each digit of the heading angle + while (line.charAt(index)!=')') { + heading.append(line.charAt(index)); + index++; + } + //heading is multiplied by negative 1 because the conversion between screen coords to inches flips the angle, so adding a negative sign flips it back + return new Pose2d(Double.parseDouble(x.toString()), Double.parseDouble(y.toString()), -Double.parseDouble(heading.toString())); + } + + + /** + * Interprets the tangent angle from a line of code that performs a robot action represented as a string. + * @param line The line of code to look for a tangent in. + * @return The angle in radians of the tangent of the line + */ + private double interpretTangent(String line) { + //In all actions, the first ')' marks + //the end of a position definition and the start of an angle definition. For example, in + //strafeToLinearHeading(new Vector2d(39.65625,26.261250000000004),0.0), the first instance of a ')' character marks the start of an angle. + //this is true for the rest of the path types as well + int index = line.indexOf(')') +2;//add two to the index, one for the ending ')', and the other for the ending ',' both after vectors or poses + StringBuilder tangent = new StringBuilder(); + //adds all the digits of the tangent angle + while (line.charAt(index)!=')') { + tangent.append(line.charAt(index)); + index++; + } + return Double.parseDouble(tangent.toString()); + } + + + +} diff --git a/src/main/java/MeepMeepDrawing/nodeEditing/MoveTool.java b/src/main/java/MeepMeepDrawing/nodeEditing/MoveTool.java new file mode 100644 index 0000000..c363195 --- /dev/null +++ b/src/main/java/MeepMeepDrawing/nodeEditing/MoveTool.java @@ -0,0 +1,39 @@ +package MeepMeepDrawing.nodeEditing; + +import com.noahbres.meepmeep.roadrunner.entity.RoadRunnerBotEntity; + +import java.awt.Point; +import java.awt.event.MouseEvent; + +import MeepMeepDrawing.RobotPath; + +public class MoveTool extends PathEditingTool { + + public MoveTool(RobotPath.PathType pathType, RoadRunnerBotEntity bot) { + super(pathType, bot); + } + + + @Override + public void mouseDragged(MouseEvent e) { + super.mouseDragged(e); + setNodeLocation(e.getPoint()); + } + @Override + public void mouseReleased(MouseEvent e) { + super.mouseReleased(e); + setNodeLocation(e.getPoint()); + } + + /** + * Sets the selected path's ending node to the mouselocation to move it + * @param p The point to move the node to + */ + private void setNodeLocation(Point p) { + RobotPath path = getSelectedPath(); + if (path==null) + return; + path.setEndLocation(p); + } + +} diff --git a/src/main/java/MeepMeepDrawing/nodeEditing/PathEditingTool.java b/src/main/java/MeepMeepDrawing/nodeEditing/PathEditingTool.java new file mode 100644 index 0000000..e596fae --- /dev/null +++ b/src/main/java/MeepMeepDrawing/nodeEditing/PathEditingTool.java @@ -0,0 +1,55 @@ +package MeepMeepDrawing.nodeEditing; + +import com.noahbres.meepmeep.roadrunner.entity.RoadRunnerBotEntity; + +import java.awt.Point; +import java.awt.event.MouseEvent; + +import MeepMeepDrawing.Node; +import MeepMeepDrawing.RobotPath; +import MeepMeepDrawing.Tool; + +public abstract class PathEditingTool extends Tool { + private RobotPath selectedPath; + private Node selectedNode; + public PathEditingTool(RobotPath.PathType type, RoadRunnerBotEntity bot) { + super(type, bot); + } + @Override + public void mousePressed(MouseEvent e) { + super.mousePressed(e); + selectedPath = getPathEndingAt(e.getPoint()); + if (selectedPath==null) + return; + //if n1 is closer to the mouse than n2 + if (selectedPath.getN1().distance(e.getPoint())