Sketchy

Early Handin: Sunday, December 8, 11:59 PM ET
Regular Handin: Tuesday, December 10, 11:59 PM ET
Late Handin: Thursday, December 12, 5:00 PM ET

Assignment Roadmap

Silly Premise

As CS15 draws to a close, you may find yourself thinking: “Self, this has been an entertaining and educational class, but how does all this relate to the real world? And why do all the projects have such boring names?” Well, all of your questions have been answered in the form of Sketchy! Sketchy is the final project with flair. Sketchy is the final project that allows you to write a program that is more than entertainment - it has some real functionality. Sketchy is the final project that will change your life forever. What is Sketchy? Sketchy is the world’s latest and greatest graphics editing program. It lets you draw to your heart’s content, create different kinds of shapes on a canvas and manipulate them in various ways. Not everyone is perfect, so Sketchy has undo and redo capabilities. And since no program worth its salt would force you to create a masterpiece in one session, Sketchy can save and load your works of art!

Collaboration Policy Reminder

If you ever have questions about the collaboration policy, refer to the collaboration policy or ask a TA!

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Note: The usage of any artificial intelligence technologies, except those explicitly endorsed by CS15, is strictly prohibited. Because of TAs reading your code and a software package we use called MOSS (Measure of Software Similarity), illegal collaboration is easily detected in CS15.

Main Concepts

Sure, this talk about the features of the program is all fine and dandy, but how will you implement all of this? We’ve broken up the project into five main stages:

  • User Interface: The first major part of your program will be getting your GUI up and running. This part of the project will allow you to be creative and craft a pretty UI using some of the more advanced features of JavaFX.
  • Drawing: The next step will be allowing the user to draw directly on the canvas with the cursor. To do so, you’ll want to use a javafx.scene.shape.Polyline.
  • Creating & Manipulating Shapes: To implement this part of the project, you’ll be using JavaFX Shapes and expanding on some of their capabilities. In order to properly rotate and resize your shapes, we’ll provide you with pseudocode.
  • Raising & Lowering Shapes: As part of this project, you will need to implement raising and lowering. Basically, if one shape is underneath a shape/line, selecting the shape and then clicking the raise button will lead to it being on top of that shape/line. Think about how to implement this both logically and graphically! You do not, however, need to be able to raise and lower lines.
  • Undo & Redo: To implement undo and redo in Sketchy, you will first need to understand the command pattern and then implement it with a Stack.
  • Save & Load: Finally, you will be implementing the ability to save and load the state of your program. To do so, you will learn about converting all of the useful information about a shape to a file format, and file parsing. This will introduce you to the important concept of file I/O, which stands for Input/Output.

Make sure to watch the demo before reading the rest of the handout! The handout focuses on how to implement the functionality seen in the demo, and being able to mentally visualize that functionality will help a lot when reading.

In our demo, shapes are selected by clicking on them with the “Select Shape” radio button selected. Here’s how the interactions work:

  • Drag with the mouse pressed to translate a shape.
  • Hold SHIFT and drag to resize the shape.
  • Drag with CTRL pressed to rotate a shape.
  • If you press SHIFT and CTRL simultaneously and drag, it should both resize and rotate the shape at the same time. This may look a little weird for ellipses, but that’s fine. These combination changes should be able to be fully undone in multiple presses of the undo button. Translation and/or rotation and/or resizing should be undoable in two/three undos (in any order).

You’re free to implement Sketchy with whatever controls you’d like as long as it makes sense (using different keys, or middle/right mouse button, for example). Just make sure you tell us in your comments and README!

New Concepts Covered

  • MouseEvents
  • Stacks
  • Saving & loading (file I/O)
  • Packages (organizing a large program!)
  • JavaFX features (ColorPicker, PolyLines, RadioButtons)

Helpful Resources

Important Prelude (READ THIS)!

Note that you do not have to complete these steps in this order, and that some steps are possible without doing the tasks listed before it in these suggested steps. For example, you will obviously have to be able to get shapes to appear before working on rotation, translation, deletion, etc., but you do not need to know how to translate a shape before being able to rotate it. If you find yourself in a pinch, you can try another step! If you are waiting in line for hours for help with rotation, try tinkering with undo/redo or saving and loading!

We cannot emphasize this enough! Sketchy is a large project, so you have to keep working on other sections of the project even as you debug one aspect. This isn’t to say you should accumulate lots of bugs and leave them for the end, but if you get stuck on something and can’t seem to get unstuck, try to shift gears and work on other areas of the project.

As with Tetris, you should start early and code incrementally. Please come to Conceptual Hours with any questions about design and implementation. Also, you’re required to come to Conceptual Hours to get your code checkpoint checked off. Come to Debugging Hours only with bugs that you can't solve on your own. The IntelliJ debugger, printlines, stack trace, and other resources we’ve provided will help you fix most problems that you face (you can refer back to the Debugging Lab and the Debugging Cheat Sheet for more information).

If you go to TA hours seeking debugging help, be prepared to show the TA extensive debugging efforts. If the TA feels that you haven’t spent enough time trying to solve the bug, they have the right to refuse you. It is in your best interest to remove yourself from the list if you resolve your bug or feel as though you haven’t debugged sufficiently, if you don’t you will get turned away and have to wait an hour to sign up again (as per the TA hours policy).

Read the ENTIRE handout from start to finish. Refer back to it when you start a new section, and everytime you encounter a bug. This handout has critical information that is easy to miss when skimmed! Remember that we make these handouts to help you tackle obstacles we know you’ll face!

Grading

The grade for this assignment will be determined by functionality (50%), design (35%), and style (15%).

Functionality

These are the features that you must implement properly in order to receive full credit on the assignment. The user must be able to:

  • Free draw with the cursor
  • Create at least two types of shapes (e.g. Ellipse and Rectangle).
  • Specify the color with which to draw lines or create and fill shapes
  • Select any created shape
  • Move or translate the selected shape
  • Rotate the selected shape
  • Resize the selected shape
  • Delete the selected shape
  • Change the layering of shapes - raise or lower the current shape
  • Undo and redo any action executed within the program using the command pattern
  • Save and load the entire state of a drawing

Read on to the design portion of this handout for the exact specs for these requirements!

Installing Stencil Code

Click here to get the stencil from Github. Refer to the CS15 GitHub Guide for help with GitHub and GitHub Classroom.

Once you’ve cloned your personal repository from GitHub, you’ll need to rename the folder from sketchy-<yourGitHubLogin></yourGitHubLogin> to just sketchy. You will have issues running your code until you make the change.

About the Stencil

As with Tetris and DoodleJump, we will only be giving you a few files in the stencil.

  • App.java is your standard App class. You’ll probably need to edit it.
  • main, commands, and shapes packages. You can think of these as subfolders that help you organize your classes (you will have quite a few by the time you finish coding Sketchy!) Read more about packages in the packages section!

You’ll also see that the commands and shapes packages contain an empty file called .gitkeep. This is just here to inform git that we want this folder to be pushed to GitHub as well - git will not otherwise add empty directories. Make sure you delete this file once you’ve added something to that package!

Coding Incrementally

After you’ve watched the demo, thoroughly read this handout (especially the Design Details) to make sure you understand the project design. Once you’re ready, start coding! It is important to code incrementally, meaning you completely accomplish one logical task before moving on to the next one.

Suggested Order for Incremental Coding
Sketchy is a large project. You will be writing more classes and more lines of code than you have for any other project in CS15. This means that it is especially important to code Sketchy incrementally! If you do not, you will be stuck with terrifying JavaFX stack traces that you may have no idea how to debug. Here’s one way to code Sketchy incrementally:

Step 1. Run the stencil code.

Step 2. Finish setting up the GUI - arrange your Panes on the screen, add all of the necessary Buttons, Labels, and ColorPicker for controls, get everything looking how you want it to.

Step 3. Make one type of shape show up in response to a mouse click. Consider giving shapes a default initial size at this stage since you haven’t implemented resize yet!

Step 4. Make other types of shape show up.

Step 5. Implement free draw with the cursor.

Step 6. Allow the user to select a shape (don’t forget about the contains method!).

Step 7. Use the ColorPicker to select a color and fill the selected shape with that color.

Step 8. Delete the selected shape.

Step 9. Translate the selected shape.

Step 10. Rotate the selected shape.

Step 11. Resize the selected shape, then update shape creation to use your resize method.

Step 12. Raise and lower shapes (layering).

Step 13. Undo/redo shape creation, color changing, raise, and lower.

Step 14. Undo/redo everything else

Step 15. Save/load drawings.

Step 16. Also remember to create a README (follow the guidelines in the README section).

Code Checkpoint

This project will have a checkpoint that you’ll need to get checked off at conceptual hours by Sunday, November 24th. Sketchy is a large project, so we want to make sure you’re on track. Here is what you need to have completed by then:

  • Finish setting up your GUI — you’ll likely need JavaFX Buttons, RadioButtons, a ColorPicker, and Labels. Check out the demo to see how your GUI might look!
  • Make two types of shapes appear in response to a mouse click.
  • Implement free draw with the cursor.
  • Be able to change at least one property of the shape based on some of your controls (we recommend doing color change!)
  • Be able to delete the selected shape.

The checkpoint is less than halfway through the project and is there to make sure that you get started. The later parts of this project can be more time consuming than the first parts so please make sure you keep working on it after you meet the checkpoint!

Minimum Functionality Requirements

Your Sketchy will have to do the following things in order for you to meet minimum functionality (REMINDER: you have to achieve minimum functionality on all projects to pass the course, and since this is the final project, you cannot hand in again after the late handin deadline!)

MF Policy Summary: In order to pass CS15, you will have to meet minimum functionality requirements for all projects. MF requirements are not the same as the requirements for full credit on the project. You should attempt the full requirements on every project to keep pace with the course material. An ‘A’ project would meet all of the requirements on the handout and have good design and code style.

To meet MF for Sketchy:

  • Two types of shapes should:
    • be able to be created and deleted
    • in some way be resized, rotated, translated
      • for minimum functionality, these shape transformations do not have to work the same way as in the demo, although they do for full credit
    • be able to be filled with a user-selected color
    • be able to be re-ordered - for minimum functionality, buttons to move the selected shape to the front/back of the canvas is sufficient; for full credit, should work as in demo/as described above
  • Lines should be able to be drawn on the canvas.
  • Undo/redo must work for fill shape, create shape, draw line, and delete shape
  • Undo/redo must work for one of translate, rotate, and resize
  • You must be able to save and load all shapes (not lines) in the scene, preserving the correct size, position, rotation, and color. Shape and line layering in saving and loading does not need to be preserved for MF. Shapes transformations also do not have to be functional after loading for MF.

Full Functionality Requirements

Beyond the minimum functionality requirements, the rest of the functionality grade will depend on the following criteria:

  • Resizing, rotation, and translation of shapes work the same way as in the demo
  • Shapes are able to be rotated and resized at the same time
  • Shape raising/lowering works the same way as in the demo
  • Undo/redo works for translation, rotation, resizing, and raising/lowering
  • Undo/redo works for combined rotation/resizing as described above
  • You are able to save and load lines
  • Shape and line layering in saving and loading is preserved
  • Shape transformation work the same way as in the demo after loading a scene

Bells and Whistles

We’ll be happy to reward plenty of extra credit to any Sketchy that goes above and beyond! Here are some ideas:

  • Extra shapes and their transformations/other shape functionalities (perhaps using JavaFX Polygons)
  • Select, move, rotate, and resize lines
  • Animation (look into JavaFX Transitions)
  • Copy and paste functionality
  • Use photos as objects (and be able to interact with them)
  • Export as image
  • Define layers (like in Photoshop)
  • Select multiple objects at once (and do operations on them simultaneously)
  • and many, many more!

Remember that you should make sure that you have a fully functional program before working on extra credit. Remember, from the Course Missive:

“Extra credit is only to be done after the original assignment has been fully completed - if you have not met the requirements, you will not receive extra credit. Extra credit may not redefine the original assignment. Additionally, priority is given at TA hours to students trying to complete minimum functionality. Do not expect to be seen for extra credit questions at busy times.”


Implementation

Sketchy Specs

Here are some further specs to help you meet the assignment requirements listed above!

Using Point 2Ds

In this project, you’ll be using many (x, y) points to set and keep track of locations of mouse presses and shapes. Writing many accessors and mutators for both x and y locations of objects can become very repetitive. If only we could keep track of both x and y locations in one variable… Good news, Point2Ds (javafx.geometry.Point2D) can do just that! Point2Ds are wrapper classes around an (x, y) coordinate location. To instantiate a Point2D, you’d do

​Point2D point = new Point2D(<x location>, <y location>);

Also, when you want to access either the x or y coordinate specifically you can use

​point.getX();
​point.getY();

Check out the Javadocs for more information! You’re not required to use Point2Ds, but they might prove to be helpful in keeping your code concise!

Free draw with the cursor

In order to let the user draw, you’ll need an object-oriented way of representing curved lines. The javafx.scene.shape.Polyline class stores an array of doubles (alternating x, y) representing points, and draws a line connecting all of those points on the pane. So a Polyline with the array {2, 3, 4, 5} would look like one straight line segment from the point (2, 3) to the point (4, 5). It’s possible to modify the double array of an existing Polyline to add new points - you can access the array with the getPoints() method. You’ll want to make your own CurvedLine class that stores a Polyline instance, so that we can add our own functionality. This class will be a wrapper class, similar to your doodle class in DoodleJump or your Square/Rectangle class in Tetris (if you had one). You’ll probably want to write a method that adds a point to your CurvedLine’s underlying Polyline. Now that we have a way to represent **CurvedLine**s, we need to add user interaction. The Pane that represents your “canvas” will have a handler (or handlers) that fires when the user interacts with it through the mouse. You’ll probably want to do something like the following:

  • When the mouse is pressed:
    • Instantiate a new CurvedLine and store a reference to it - i.e., in a variable (think about what type of variable it should be). You will need a reference to this particular instance of the CurvedLine so that you know where additional points must be added to.
    • After a mouse press, your CurvedLine will only contain two doubles, referring to the x and y position of the mouse press.
    • At this point you should also add your Polyline graphically to the pane it is contained in.
  • When the mouse is dragged:
    • Add a new point (pair of doubles) to that Polyline’s list of doubles, passing in the current x and y position of the mouse.
    • Your Polyline instance will maintain the list of its points, so every point you add should continue to appear graphically as well.

Create at least two types of Shape

In Sketchy, you are required to implement two or more types of shapes which the user can add to the Pane representing your canvas. You will have a group of RadioButtons (see the JavaFX documentation) that allow the user to select what kind of shape they want to draw (see the demo). If a shape creation button is selected, then once the user clicks upon the canvas Pane, the selected shape is created at the position of the click. The user will drag the mouse to set the shape’s initial size, just as they would resize an existing shape. You should not just create a shape with some constant size. Use classes in the JavaFX sShape package to model shapes as you have done in previous projects. Consider, however, that you will want some functionality in addition to the basics provided by the JavaFX Shape class, so creating your own classes to represent shapes would be a good idea. We recommend creating a separate SketchyShape interface and implementing it in specific shape classes that contain JavaFX Shapes.

Specify the color of shapes

The basic requirement for color selection in Sketchy is to use the javafx.scene.control.ColorPicker (see Javadocs), though you are more than welcome to use some other method of selecting colors. The ColorPicker has the getValue() method, which will allow you to determine the currently selected color value. Once the user has selected a color, anything added to the canvas must appear in this color. Additionally, you should be able to change the color of your shapes on the Pane to whatever color is indicated by the ColorPicker by selecting the shape and clicking the Fill button.

You are not required to be able to change the color of the lines after they are drawn, though this can be done for extra credit.

Select any created shape

You should be able to select any of your shapes by clicking on it. A shape should indicate that it is selected by drawing a border (hint: stroke!) around it. Your lines are not required to be selectable. Once a shape has been selected, the shape manipulation buttons should act on the selected shape. If you click off the shape or start drawing a shape or line, then the shape should become deselected and the border around it should disappear. Note that in the case of overlapping shapes, the shape on top (and only that shape) should be selected. There are several strange edge cases for shape selection - think through these!

A note about the contains method

JavaFX Nodes have a built-in contains() method which you can use to check whether a mouse click is within the original bounds of a shape. However, this method does not take rotation into account. When a shape is rotated, its bounds change, and a point that was contained in its original, unrotated bounds may not be contained in the rotated shape. In order to correctly use this method, then, we’ll have to rotate the given point about the shape’s center by the same degree the shape is rotated, and check if the Node contains the rotated point.

But how do we rotate a point around another point? We’re providing you with pseudocode for this method. The method takes in a pointToRotate, and rotates the point by angle degrees around the point rotateAround.

function rotatePoint(pointToRotate, rotateAround, angle):
    sine = sin(toRadians(angle))
    cosine = cos(toRadians(angle))

    // First we move our rotateAround to the "origin" so that
    // pointToRotate will rotate around (0,0)
    point = Point(pointToRotate.x - rotateAround.x, pointToRotate.y - rotateAround.y)

    // Next we rotate the point by the input angle
    point = Point(point.x*cosine + point.y*sine, -point.x*sine + point.y*cosine)

    // We return the point to its initial offset
    point = Point(point.x + rotateAround.x, point.y + rotateAround.y)

    return point

Move/translate the selected shape

You should translate the selected shape as the mouse is dragged. You’ll want to create a translate(...) method that takes in two points – one “previous” mouse point, and one “current” mouse point. Think about how you could use Point2Ds here to make your code more concise! In your translation method, you’ll determine the difference in x (dx) and difference in y (dy) between the two points and adjust the shape’s location by those values.

Note: do not use the setTranslateX and setTranslateY methods. This will make your life far, far more difficult than it needs to be.

Rotate the selected shape

Just like you did with translation, you’ll want to create a rotate(...) method that takes in two points, the previous mouse point prev and the current point curr. Calculate the angle between the two points about the center of the shape and then increment or decrement the shape’s rotation by this angle. You’ll find that Java’s Math class has some very helpful static methods. The angle between two points is given by:

atan2(prev.y − center.y, prev.x − center.x) − atan2(curr.y − center.y, curr.x − center.x)

Note: Watch out for conversion between degrees and radians!

Resize the selected shape

Resizing the selected shape requires again writing a method that takes in the current mouse point curr. In our demo, holding down SHIFT and dragging the mouse will resize the selected shape. For Full Functionality your shapes should resize in the same way as the demo. The center of the shape should be fixed, and the shape should resize from the center. Notice that resizing an unrotated shape is relatively straightforward. We just increase (or decrease) the shape’s width and height by dx and dy while maintaining its center location. How will we handle resizing the shape once it has been rotated?

Similarly to what we had to do for the contains method above, we have to rotate the input curr about the shape’s center before we can use them to calculate dx and dy. To understand the reasons behind this, consider this example:

You have a Rectangle that’s rotated 90 degrees, and you drag the mouse vertically to make the shape bigger in the y direction. You might think that you could just update the shape’s height by dy, but because the shape is turned on its side, changing the shape’s height actually makes it longer horizontally. If we first rotate the input points by 90 degrees, we will get new points that differ by the same amount, but in the x direction instead. dx corresponds to change in width, so we update the shape’s width accordingly, which actually makes it bigger vertically, giving us the desired result.

Now we can write our resize(...) method to take in one point, curr point which is the mouse’s current location. The resize method will work as follows:

  1. Store the value of the shape’s center
  2. Using the above rotatePoint() pseudocode, rotate curr around the center of the shape by the shape’s angle of rotation.
  3. Compute the distance between (rotated) curr and the center of the shape in the x-direction (we’ll call this dx) and in the y-direction (we’ll call this dy).
  4. Update the shape’s dimensions using dx and dy
         a. For the rectangle, dx is half of the rectangle’s width and dy is half of its height.
         b. For the ellipse, dx is the radius in the x-direction and dy is the radius in the y-direction.
  5. If the center of the shape has changed, update the shape’s location so that the center remains fixed. Remember that some Shapes set their location based on the top left corner and others based on the center.

Delete the selected shape

Once a shape is drawn, you should provide the option for the user to remove it from the canvas. Your shapes will be stored in some sort of data structure(s), so consider how to properly remove a specific element. How will you remove a shape from the data structure(s) and ensure that it is no longer displayed on the canvas?

Remember that when you delete a shape and then undo that action, you will want that shape to preserve the layering it had before it was deleted. Graphical layering is related to the order in which nodes are added to a Pane - think about how you might keep track of which index in the ArrayList of a pane’s children a particular shape was added to.

Raise/Lower shapes

When two shapes or lines overlap, whichever is added to the Pane’s List of children last is rendered on top.

Your “raise” button will bring a shape one layer forward such that it is graphically above the shape/line that was previously above it. Similarly, your “lower” button should move a shape one layer backwards such that it appears graphically under the shape/line that was previously under it. Consider how you will accomplish this change both graphically (on the Pane) and logically (in your data structures).

Your data structures should store shapes and lines in the same order that the pane’s list of children does. This is important for undo/redoing this command (and undoing deletion) and for saving/loading. When changing shape layering, you’ll need to change:

  1. The index the shape’s node has in the Pane’s list of children.
  2. The index of your shape in your other data structures (will likely differ between data structures).
Hint

You can get an object’s index in an ArrayList using the list’s indexOf method. Make sure the object you’re passing in is actually in the list!

How can we undo raising a shape? Lower it! (and vice versa). To undo deletion, however, you will need to know the indices the shape previously had in all relevant data structures in order to perform an accurate undo.

Hint

JavaFX panes’ lists of children have some strange rules that mean you cannot easily swap two elements in the list. Specifically, the list cannot have two of the same Node and cannot have null elements. Therefore, the best way to change a Node’s index in the list is to figure out the index it needs to move to, remove the Node, and then add it back in at the proper index. Think carefully about this and consider drawing out a few examples. You might also find that this remove-and-then-add method is also the easiest way to change the index of an object in your other data structures.

A note about shapes and lines (and pseudocode!)

Lines add a degree of complexity to how we handle layers. If you had a data structure that stored all your shapes in the right order, adjacent shapes in that data structure may not be adjacent in the Pane’s list of children - there may be lines in between! Thus, in order to correctly change the index of a shape in its data structure, we need to consider its index in the Pane’s list of children as well as the index of the next/previous shape in the list of children. This can become quite complicated to deal with, so we’ve given you pseudocode to handle this situation. If this is confusing, please come to conceptual hours to talk through it!

The following pseudocode is to calculate the change in shape index when raising a shape and only needs minor changes to deal with lowering! The crux of it is as follows: if a shape is next to another shape in the shapes data structure and the Pane’s list of children, then it can be moved in both. If a shape is next to another shape in the shapes data structure but not next to the other shape in the Pane’s list of children, the difference between their indices is greater than 1 , and there are line(s) between the two shapes! So it should only be moved in the Pane’s list of children.

currShapeInArray = index of current shape in shapes data structure
currItemInPane = index of current shape in the Pane's list of children
nextShapeInArray = currShapeIndex + 1

// if the current shape is not at the top of the pane already
if currItemInPane is less than the size of the pane's list of children:

    // if the current shape can move in the shape data structure
    if currShapeInList has one more index to move up into in the shape data structure:
	
        // get the next shape in the shape data structure
        nextShape = the next shape from shape data structure at nextShapeInArray
	
	// if the difference between the index of the current shape in the data
        // structure and the next shape in the data structure is only 1, then
        // they're adjacent in both the shape data structure and the Pane's
        // list of children. if the difference isn't one there's at least one 
        // line between these shapes, and we shouldn't update its index int he 
        // shape data structure, but we should still update it in the Pane's 
        // list of children (and any other data structures that store both 
        // shapes and lines)
        if nextShape’s index in the pane - the current shape’s index in the
 	pane is equal to 1:
    
    	 	// update the shape in the shape data structure
               current shape's location in shape data strcutre = nextShapeInArray
               
    // update the shape in the Pane's list of children (and any other data strcutures that store both shapes and lines)
    current shape index = currItemInPane + 1

Command Pattern

The Command Pattern is used to model commands and actions. Modeling commands may seem pretty abstract at first, but that’s what CS15 is all about: abstraction! Using polymorphism, you’ll abstract these commands into an interface or abstract superclass, and then define a class for every command you want to model.

Think of commands as actions or things to be done. In Sketchy, the user can draw with the pen, create shapes, rotate shapes, scale shapes, move shapes, change the color of shapes, and more. Consider these as actions that the user wants to perform.

The commands you write should be able to be undone and then redone. Think about what a command needs to know about in order to undo its action. When you want to undo an action, you want to return the program to the state it was in before the action was performed. So, your commands are probably going to need to store some information about the state of the program before and after the action occurred. The information that needs to be stored depends on what kind of command it is.

For example, if you were modeling a command that changed a shape’s color, you’d probably want to store the shape’s color before and after the command was performed. You could then use this information to undo and redo the command.

Undo and redo any action executed within the program

Let’s take a look at how undo and redo work for the user. Undo should “reverse” the last action that was performed. This last in, first out pattern should remind you of a Stack. Each command that a user performs should be pushed onto a Stack of previous commands. Each time a user tries to undo an action, simply pop off the last command that was performed and undo it.

But how do we undo and redo different kinds of actions? This is where you will use your command pattern. Although undoing a change in color is different from undoing a change in size, this difference is captured by the undo() and redo() methods of each implementing / inheriting class of your Command interface / superclass. Because of polymorphism, we can have a Stack of Command objects and when you call undo() or redo() on one of its elements, the correct implementation of the undo() or redo() will be invoked.

Since each of the commands know how to undo and redo, you don’t have to worry about which command you happen to be dealing with. You do know, however, that you are performing them in the right order because you get them off the top of the Stack.

What about redoing? First note that redoing is only available after undoing. Redoing is doing again those things that you’ve undone, in the proper order. Again, a Stack will save us! After undoing a command, you should keep track of it in a similar way that you’ve kept track of all the normal commands: push it onto a Stack. This time, however, use a different Stack.

Take a look at the following diagrams to see how to put this all together:

Performing a new action

Screenshot 2024-11-13 at 1.59.30 PM

Undoing an action

Screenshot 2024-11-13 at 1.59.42 PM

Redoing an action

Screenshot 2024-11-13 at 2.00.02 PM

Save and load the entire state of your drawing

Sketchy drawings are collections of shapes and lines, each with appropriate settings and values. Saving this drawing is simply the act of keeping a record of each line and its color, and each shape and its attributes. Loading a drawing consists of doing the opposite: reading in which shapes and lines to draw, and setting their attributes.

You’ll need your format to include some mention of whether the object you’re referring to is a shape or just a line. It turns out that for covering base specifications, each shape you use should only have a very limited set of attributes. Each shape will probably have the following:

Parameter Values Data Types
Type of object Shape name String
Location x-coord, y-coord double, double
Dimensions degrees double
Color red, green, blue int, int, int

Each line will probably have the following:

Parameter Values Data Types
Type of object line String
Color red, green, blue int, int, int
Array of points x-coord, y-coord, double...

This information is all your program needs to reconstruct the exact same shape or line when it loads the file later. (You can also store additional information that might help you load shapes and lines!)

If you know that each shape or line is described by the above parameters, then you should be able to go through a list of these, making a new shape or line for every set of data you read.

A short file with three objects may look like this:

rectangle 15 10 70 40 0 255 255 255 ellipse 50 50 20 20 0 255 0 0 line 255 0 0 6 50 55 60 60 65 70

Assuming the pattern of variables described above, the first line of this file describes a rectangle at position (15, 10), with a width of 70 and height of 40. It’s rotated 0 degrees and is colored white. The second line of the file describes an ellipse at position (50, 50), with a width of 20 and a height of 20 (it looks like a circle), rotated by 0 degrees and colored red. The third line of the file describes a red line with 6 doubles forming points at (50, 55), (60, 60), (65, 70).

Remember that storing each line will require storing each of its individual coordinates (of type double). Don’t worry about the length of these entries in your saved file – it takes a lot of space to store all of those points, so it makes sense.

Remember that the above specifications are only suggestions. You have the final say on what your file format will be. You may find that rearranging the parameters would suit you better – that’s fine! If you are considering a file format or parsing technique that is far different from our suggestion, you should consider talking to a TA about it before implementation.

Design Considerations

For full functionality, It’s important that saving/loading a drawing preserves the layering of shapes and lines on the canvas. The order in which Nodes are added to the Scene Graph (the list of children) determines the layering of shapes and lines on the screen. So to save your entire collection of Shapes and Polylines, you might consider just iterating through the Pane’s list of children. However, this will not work. When we add Rectangles, Ellipses, Polylines, etc. to the children list, we know their specific type. However, the children list stores objects of type Node, so if we iterate through that list, we will only be able to refer to the objects as Nodes, not as their specific types. Nodes do not have the properties listed above, such as color, width, height, and rotation.

Since we can’t use the list of children, what data structure can we use to store everything (shapes and lines) that we need to save, while preserving the same order as the Pane’s list of children? Every object in this data structure must be of the same type, but what type should that be? CurvedLines and SketchyShapes don’t inherit from the same supertype, so we’ll need some way of relating these two objects for the purposes of saving…

What do CurvedLines and SketchyShapes both need to know how to do? They both need to know how to save themselves! So there’s one possible way to relate these two different types by having them implement/inherit from the same supertype, but should this supertype be an abstract class or an interface? What method(s) would this supertype need to make it possible to save every implementing class?

sketchySupport.CS15FileIO

Great, now I know what to do to save my shapes and lines, but how the @#$% am I going to do all that? FileIO!!

The cs15.fnl.sketchySupport.CS15FileIO class is a support class that will do the nitty gritty parts of writing to and reading from files. You don’t need to subclass it. You’ll just create an instance of it and call its methods. To find a list of CS15FileIO’s methods, consult the Sketchy FidxleIO Javadocs. Make sure to read these carefully before moving on.

So here is how you would use the CS15FileIO class to make a file that contains the phrase “a b c 1 2 3”:

CS15FileIO io = new CS15FileIO();
io.openWrite(filename) // where filename is a string representing the name of the file you want to write to.
io.writeString("a");
io.writeString("b");
io.writeString("c");
io.writeInt(1);
io.writeInt(2);
io.writeInt(3);
io.closeWrite();

Notice the last step. It’s very important. If you don’t call closeWrite() when you’re done, nothing will print out to the file. Also notice that the order of these method calls is what determines the order of the information in the file. This is sequential file I/O. Calling one of the write methods just adds that information to the end of what has been written to that file since it was opened. A very important thing to realize, though, is that when you call openWrite(filename), the contents of the file represented by the String filename will be deleted and replaced with whatever you write in it. Opening a file for writing and then writing to it DOES NOT just put what you write at the end of that file. Look at the method public static String getFileName(boolean save, Window stage) to help you out with the initial steps of saving and opening files.

Ok, so now you can write information to a file. But you’re also going to need to read information from a file to be able to load in Sketchy. So let’s read the contents of the file that you just wrote to and store the information in six variables. We assume for right now that you know that you are going to be reading three Strings followed by three ints.

  • First, we declare three String variables: string1, string2, string3 and three int variables: int1, int2, int3.
  • Now we are ready to read the file, so we call openRead(filename) where filename is a String representing the name of the file that we wrote to before.
  • Now, let’s read the three strings: string1 = fileIO.readString(); string2 = fileIO.readString(); string3 = fileIO.readString(); where fileIO is a reference to the sketchySupport.CS15FileIO instance that you created.
  • Now for the ints: int1 = fileIO.readInt(); int2 = fileIO.readInt(); int3 = fileIO.readInt();
  • call closeRead();

Now, let’s talk about how to apply these methods to saving and loading in Sketchy.

Saving will involve iterating over all of the shapes and lines that you’ve stored and telling each to save itself. One way of doing this is to give the objects that represent your shapes and lines a method that takes in a CS15FileIO object with which it saves itself. What your shapes and lines should be writing to the file are the values of their attributes described above.

Remember that how you write to a file will depend on how you expect that file to be read. Therefore, make sure you do some pseudocoding for both the saving and loading processes before writing them.

To load a file, you will basically open a file and work through it, shape by shape, until you reach the end. (Before doing this, you’ll want to logically and graphically remove everything currently on the canvas). Start out by using the CS15FileIO class to open a file for reading. While (wink, wink) there is more to read in the file (think fileIO.hasMoreData()), you should assume that there is another shape to parse out. Again, using your “smart shapes”, parse out the expected data types and use them to construct a new shape.

Based on the format you used to save your shapes and lines, you will know in what order their attributes are stored in the file. If you know the next piece of data will be an int, use the readInt() method to get the next integer. If it will be a double, use the readDouble() method to get the next double. If it’ll be a String, use the readString() method to get the next String. You’ll probably want to use the name of the shape to determine which type of shape you’re going to construct. This gives you a way to know what the next data type must be, because you yourself designed the file format!

MouseEvents

MouseEvents are objects that contain information about a mouse input event, and they are extremely important for this project.

You will want to define EventHandlers that handle user mouse input. It is important to note that there are different types of MouseEvents. See the Field Summary in the MouseEvent Javadocs for a list of them (you will not have to use all of the listed EventTypes). Here’s a little bit of information about a few you might be interested in:

  • MOUSE_PRESSED is triggered when the mouse is pressed. It only refers to the initial press - pressing and holding will not produce additional MOUSE_PRESSED events.
  • MOUSE_DRAGGED is triggered when the mouse has been pressed and dragged
  • MOUSE_RELEASED is triggered only when the mouse is released

A way to do this is to define a different EventHandler for each type of MouseEvent you will need to respond to. Adding these EventHandlers is similar to how we’ve handled KeyEvents in the past - the same way we used pane.setOnKeyPressed((KeyEvent e) -> methodCall()), there are relevant methods for adding EventHandlers for MouseEvents (hint: setOnMousePressed, setOnMouseDragged, setOnMouseReleased). Check out the Node Javadocs for the full list of methods!

You can find out information about what the user did with the mouse by calling methods on a MouseEvent. For example, you will need to get the location of MouseEvents in order to resize and rotate the selected shape. To get the location of a MouseEvent, just call getX() and getY().

Finally, you can use some of the methods of the MouseEvent class to detect which mouse button was pressed, or what keys were held down in conjunction with a click. They include:
public final boolean isPrimaryButtonDown(); (AKA left mouse button)
public final boolean isControlDown();
public final boolean isShiftDown();

So, for example, if you want to check if the left mouse button is the one that was used, you can just say:
if (e.isPrimaryButtonDown())
where e is a MouseEvent. Check out the MouseEvent Javadocs for more information and more methods you can use.


Design

Packages

You might have noticed that we’ve given you a few extra files: the main, commands, and shapes packages. A package is a way to organize classes when you have a large program. In fact, you’ve been using packages this whole time! When you cloned Tetris from GitHub and renamed it, that became the tetris package! Similarly, we now have a sketchy package with other packages within it.

The main package can be thought of as the control center for your program, containing the App.java class as well as other general-purpose classes. The commands package will contain the classes that represent commands, such as creating the shape or resizing. The shapes package will contain the classes modeling the shapes and lines on the Sketchy canvas.

Note that because of our multi-package structure, typing javac *.java will not compile your code properly—it will be best to make use of the green play button for this project. Having these packages will make it easier for you as you’re incrementally coding to keep track of all your different classes. Also, since these packages are mainly for organization, you are free to alter them in whatever way makes most sense to you, as long as it’s still a coherent design. Just make sure to explain your choices in the README!

In previous projects, all of your classes have been inside a single package. Now with multiple packages, the first line of each file will say

package sketchy.<package name>;

You can see this in the top line of the App class we gave you, which reads

package sketchy.main;

Since App is in the main package. When it comes to instantiating classes in a class in one package that are within a different package, we need to use import statements! Say we’re coding a class in the main package and we want to instantiate a class that is in the shapes package. Above the class header comment, you would write

import sketchy.shapes.<name of class you want to import>;

You can also hover over the red error that shows up when you try to instantiate without importing and click the “import class” button.

SelectOptions and Enums

An enumerated type (or enum), like a class or interface, is a specific programming pattern that can abstract groups of constants. (in your case, such as the selected editing mode on the radio buttons!) Enums bear similarities to both objects and primitive types. An enum is defined with a set of constants, conventionally named in all caps. Below is an example of how to declare an Operation enum type:

public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;
}

A unique property of enums is that they can be used in a switch statement like ints (though they are not ints). For example:

Operation currOperation = Operation.PLUS;
public double eval(double x, double y) {
    switch(currOperation) {
        case PLUS: return x+y;
        case MINUS: return x-y;
        case TIMES: return x*y;
        case DIVIDE: return x/y;
    }
}

Switch statements are particularly useful with enums. The method above uses an example of a switch statement that switches based on the value of the particular instance of the enum of type Operation. Depending on the type of Operation (either PLUS, MINUS, TIMES, or DIVIDES), the method performs a different mathematical operation on its parameters. Switch may be useful in some of your methods, including your MouseEvent handlers and in any code that changes its execution based on which RadioButton is selected.

You cannot instantiate enums with new. To get a specific value, use something like Operation.PLUS or Operation.MINUS as you would with a Constants class. For example, if you have a method that takes in an Operation as a parameter, then Operation.PLUS would be an acceptable value to pass in.


Style

Refer to the CS15 Style Guide for the specific style guidelines along which your code will be graded.

Handing In

README

In CS15, you’re required to hand in a README file (must be named README) that documents any notable design choices or known bugs in your program. Remember that clear, detailed, and concise READMEs make your TAs happier when it counts (right before grading your project).

You are expected to create your own README file. Please refer to the README guide for information on how to create a README, what information your README should contain, and how you must format it. In addition to describing your design choices, if you decide to implement any extra credit, please detail it in an EXTRA CREDIT section of your README.

At the bottom of your README, add the approximate number of hours you spent on this assignment. This will be used only to average how long the assignments are taking students this semester, and it is completely anonymous.

Handin

To hand in your assignment, follow these steps:

  1. In a terminal, move into the sketchy folder.
  2. Add, commit, and push your code to GitHub.
  3. Type the command rm *.class (Mac) or del *.class (Windows)
    a. This will remove the .class files that are created when compiling so that your submission only includes .java code files.
  4. Submit your Sketchy GitHub repository to Gradescope
    a. Reminder: You do not need to submit a class diagram or a interface/inheritance diagram for this project!

If you do not submit all the proper files to Gradescope, we will allow you to resubmit with a 5 point penalty, only if you’ve pushed your code to GitHub prior to the deadline. If your code wasn’t pushed to GitHub by the deadline and you submit incorrectly to Gradescope, we will grade whatever was submitted.

You can submit as many times as you want prior to the deadline, and only your most recent handin will be graded. If you handin before the deadline and again after the deadline, the submission will be counted as late.

Do not include any identifying information on your handin (name, login, Banner ID) as we grade anonymously. Including identifying information will result in a deduction from your assignment.