Assignment 1: Shapes

Upon completion of this assignment, you will have a working Haskell program that uses the CodeWorld API to draw various colourful shapes on the screen, including lines, polygons, rectangles, circles, and ellipses. We have prepared a basic framework for you to start this assignment.

Learning Objectives

  • Mastering guards and case statements
  • Working with lists
  • Programming with events
  • Applying abstraction and functional decomposition

Getting Started

It’s okay if you don’t feel like you have mastered all of the learning objectives at the start of the assignment. This assignment is designed to be worked on while you are learning about these concepts, and provides guidance to your learning throughout.

Setting things up

  1. Go to https://gitlab.cecs.anu.edu.au, and login. After that, come back to this page and continue with the next step.

  2. Go to the assignment’s gitlab repository: https://gitlab.cecs.anu.edu.au/comp1100/comp1100-assignment1.

  3. Click on the Fork button to create your own private version of the assignment, so you can work on it independently from everyone else.

  4. Click on your name and wait a little while.

  5. Your web page should automatically refresh, and—if everything went well—display your own repository containing the assignment files. You can check this by looking above the banner “The project was successfully forked.”, it should display your name instead of comp1100.

  6. Next to the “Fork” button in your repository, there is a button which says “SSH”. Click on it and switch it to “HTTPS”.

  7. Copy the resulting URL.

  8. Open IntelliJ. If your labs project opens up then close the window: click on the menu File at the top of the IntelliJ window and select Close Project.

  9. In the IntelliJ IDEA window, click on Check out from Version Control and select Git.

  10. Paste the URL that you copied above into the Clone Repository dialogue box as the Git Repository URL. Set Parent directory to be ~/comp1100, and the directory name as assignment1.

  11. Click Clone and follow the same IntelliJ steps as Lab 2 starting from the clone step.

  12. Make sure you add the upstream remote with the following command in the Terminal:

git remote add upstream https://gitlab.cecs.anu.edu.au/comp1100/comp1100-assignment1.git 

Framework

The Cabal build tool

For this assignment, we are using cabal: a build tool for Haskell that avoids the need to build all of the pieces of your project separately.

There are several useful commands for cabal:

  • cabal build

    This will compile all of the necessary Haskell files needed for the assignment.

  • cabal run:

    This will run main program, compiling files as necessary.

  • cabal repl

    This is probably where you will be spending most of your time trying out your code. REPL stands for Read-Eval-Print Loop. This will launch a GHCi environment and load the main program.

  • cabal clean

    This will delete any build files and can be useful if the files become corrupted or you just want to build everything afresh. This will not delete any important files such as your source code programs.

  • cabal reconfigure

    This will reconfigure the project for the operating system of your computer. Sometimes you will need to do this after upgrading the OS or Haskell itself. Otherwise there are not many reasons to do this.

You will execute these cabal commands in the top-level directory of your project: ~/comp1100/assignment1 (i.e., the directory you are in when you launch the IntelliJ Terminal tool for your project).

Modules

This assignment has a framework that divides the functionality into three primary files in the src folder: Model.hs, View.hs, and Controller.hs. This partitioning of functionality is called the model-view-controller software architecture. It is a common pattern used in implementing graphical user interfaces.

The reason that the program is broken up like this is to increase readability (so that your tutors can mark more efficiently) and maintainability (so that you can find and fix problems more easily) by grouping related functions together. Moreover, there may be some functions in one module that other modules do not need. With modularisation, you can control what is exposed to other modules and what you want to use when importing.

Model

The model defines the abstract problem domain, independently of the user interface. In this assignment the model allows us to capture the user’s intent to create shapes using shape-drawing tools that can be selected by the user, and to draw those shapes on screen.

View

The view defines how we render the model for presentation to the user, in graphical form. In this assignment we will be using Codeworld API functions for drawing shapes.

Controller

The controller implements the GUI-based mouse and keyboard input functionality of the application, allowing users to control the application.

Files

The tasks for this assignment are split into 2 parts: A and B. Part A completes the view functionality, while Part B completes the controller functionality. You will work in just one file for each part of the assignment, as follows:

  1. Part A and Part B: src/Model.hs

    This file defines types Coords, Shape, Tool, ColourName, and Model.

    Coords represents points as a pair of (x,y) coordinates in the Cartesian plane. Each coordinate is represented as a Haskell Double floating point value.

    A Shape is an abstract representation of one of a variety of shapes (lines, polygons, rectangles, circles, and ellipses). For example a rectangle is described by the coordinates of its opposing corners, a polygons is represented by its vertices, a circle by its centre and a point on its circumference, etc.

    Tools are used during user interaction to record the actions of the user with respect to the given tool. For example, when using the line tool, the user will first specify the starting point for the line. When using the circle tool, the user will first specify the centre point.

    When drawing a shape, users can also select from a set of ColourNames. The ColourName type enumerates the available colours.

    The Model type captures the state of the application, including all the coloured shapes that have been drawn by the user, as well as the current tool and colour being used to draw with.

    The initialModel value represents the state of the application where there are no shapes, the line tool is enabled, and the colour to be used is black.

    You shouldn’t need to modify this file. However, if you write any functions that are accessories for the types defined in this file, feel free to add them here.

  2. Part A: src/View.hs

    This is where we deal with rendering the model as a graphical display, converting abstract shapes into pictures to be displayed. Your work in Part A will be in this file.

  3. Part B: src/Controller.hs

    This is where the user interface is defined, handling events generated by the user (key presses, mouse presses, etc.) and updating the model accordingly. Your work in Part B will be in this file.

  4. Putting the parts together: src/Main.hs

    This is the main function of the program, which glues together all of the components to create the whole interactive application.


Before we head into programming the assignment, we will start off with a few warm-up exercises. These are not assessable and are completely optional so you can skip ahead to Part A if you already understand them. However, we included these exercises so that you can refresh your memory of the CodeWorld API (the graphical user interface library we are using) from Lab 4, as well as basic Haskell programming.

If a task does not have “Assessable” in its title in this specification, you need not submit a solution for it.

Warm-up Exercises

  1. Execute a program

    Remember the joke in lecture about nobody executes Haskell programs? Well. Let’s change that. Without editing anything, try the cabal run HelloWorld command in the Terminal tool. You should see something like the following output:

    ~/comp1100/assignment1$ cabal run HelloWorld
    Resolving dependencies...
    Configuring Assignment1-0.4.0.0...
    Preprocessing executable 'HelloWorld' for Assignment1-0.4.0.0..
    Building executable 'HelloWorld' for Assignment1-0.4.0.0..
    [1 of 1] Compiling Main             ( src/HelloWorld.hs, dist/build/HelloWorld/HelloWorld-tmp/Main.o )
    Linking dist/build/HelloWorld/HelloWorld ...
    Running HelloWorld...
    Hello world! 

    The file src/HelloWorld.hs contains the Haskell “Hello World” program. We can tell that this is a program because it contains a definition of the main function, which simply calls putStrLn to print out Hello World! to the terminal. To run this program you could of course simply load src/HelloWorld.hs into GHCi and call the main function by hand, but the cabal run HelloWorld command does the work of compiling the program into a machine executable and then running that program, without you having to do anything else. You can see the steps it takes to prepare and run the program in the messages about configuring, preprocessing, building, compiling, linking, and finally running:

    • Resolving dependencies makes sure the libraries on which this program depends have been installed (such as CodeWorld).
    • Configuring figures out the steps to take in building the program.
    • Preprocessing gathers information for building.
    • Building compiles the components of your program into machine code.
    • Linking connects all of the components together to make a machine executable.
    • Running your program is the last step.
  2. Draw Something

    For this assignment, we want to produce more interesting graphical output to a browser window instead of boring text to the terminal. Of course, text can also be rendered graphically, so let’s switch gears from terminal output to graphical output and consider the CodeWorld equivalent of the “Hello World” program. We will still generate some text but we will now display it as a graphic in a browser window.

    Take a look at the src/HelloCodeWorld program. First, note that we import two functions from the CodeWorld library: text and drawingOf, and a function from the Data.Text library: pack. The main function calls drawingOf, which takes a Picture p as argument and draws it on the screen. The picture p is created using text, which takes a Text t as argument and creates a Picture. The Text t is created using pack which converts its String argument s (which has the value "Hello World!") into a Text.

    It is unfortunate that CodeWorld uses Text instead of directly using String, but values of these types can always be converted back and forth using pack and unpack:

    pack :: String -> Text unpack :: Text -> String 

    You can run this program using the command cabal run HelloCodeWorld in the Terminal where you will see something like:

    ~/comp1100/assignment1$ cabal run HelloCodeWorld
    Preprocessing executable 'HelloCodeWorld' for Assignment1-0.4.0.0..
    Building executable 'HelloCodeWorld' for Assignment1-0.4.0.0..
    [1 of 1] Compiling Main             ( src/HelloCodeWorld.hs, dist/build/HelloCodeWorld/HelloCodeWorld-tmp/Main.o )
    Linking dist/build/HelloCodeWorld/HelloCodeWorld ...
    Running HelloCodeWorld...
    Open me on http://127.0.0.1:3000/ 

    Open the URL in a browser as instructed, to see the result.

    Now, try changing this program to draw a rectangle of some size instead of the string “Hello World!”. You will likely want to use the rectangle function from CodeWorld.

  3. Make it solid

    So you’ve just drawn an outline of a rectangle, what about a solid rectangle? Or a solid circle?

    Refer back to the CodeWorld API if you are stuck.

  4. Place it somewhere else

    You may have experimented with drawing solid rectangles and circles, but they all appear to be in the centre of the screen. If you want it to be shifted horizontally or vertically, what should you do? Here are some hints:

    • Think maths.
    • If you go to a place where you don’t speak the language, you need someone or a device to …?
    • Check out the CodeWorld API.
    • It starts with the letter “t”.
  5. Colour it in

    Eventually you will need to be able to draw shapes of different colours. So let’s try to make your horizontally and vertically shifted rectangle or circle not boring black. For example… red?

    Hint is in the title.

  6. More than one shape

    If your program can only display one shape at a time, it’s a little bit boring. Try to draw something more interesting, such as a circle with a radius of 2 centred at (3,3) and a square with sides of length 4 centred at (3,6) on the same canvas.

    Hint: You can combine two pictures using the (&) operator.

  7. Checkpoint: Hello World!

    It is always a good thing to checkpoint your work regularly, committing and pushing back to GitLab, so you don’t have a catastrophe if your dog eats your laptop. Now would be a good time to do that.

    i. Commit all of your changes using git commit:

    ~/comp1100/assignment1$ git commit -a -m "Finishing Hello World!" 

    ii. Push your commits back to your remote GitLab repo using git push:

    ~/comp1100/assignment1$ git push 

    iii. Make sure that everything is in place by going back and checking that your comp1100-assignment1 project on GitLab at https://gitlab.cecs.anu.edu.au contains all of your work so far.


Part A: Shapes (Assessable) [30 marks, redeemable on Part B]

In Part A we will begin by taking models of shapes and rendering them as pictures. Each shape will be represented as a value of the Shape type defined in src/Model.hs. Your task is to take different shape instances and create a CodeWorld Picture that can be displayed on the screen. All of your work for Part A will be in the file src/View.hs. You can test your code along the way by using the command cabal repl View in the Terminal to load up GHCi with your work, and then evaluating appropriate Haskell expressions that exercise your code. Don’t forget to :reload whenever you change src/View.hs.

By now, you should have a rough idea of:

  • How to import other modules.
  • How to use the CodeWorld API documentation.
  • How to use functions from the CodeWorld API.
  • How to run a program using cabal.

With this knowledge in hand, let’s dive straight in. You will complete the program for drawing Lines first, then come back for other shapes later. They can wait.

  1. Line To Picture

    Let’s look at Shape more closely:

    data Shape = Line Coords Coords | Polygon [Coords]
      | Rectangle Coords Coords | Circle Coords Coords | Ellipse Coords Coords deriving (Show) 

    More specifically Line Coords Coords. This tells us that a Line is represented by two end-points. This is an abstract representation of a line. To turn it into a Picture, we will need to use some of the functions from CodeWorld. More specifically: consider the function polyline, which takes a list of coordinates and draws a sequence of line segments between each successive pair in the sequence. You can use this function to draw a straight line by having only two coordinates in the list.

    Your task now is to edit the function shapeToPicture :: Shape -> Picture in file src/View.hs to convert an abstract line into a CodeWorld Picture, by pattern matching on the Shape argument to extract a Line and its end-points. For now focus on lines only, other shapes will be dealt with later.

    If you haven’t already loaded View.hs into GHCi, do so now with the Terminal command cabal repl View, or if you have then simply use the GHCi command :reload to refresh GHCi. Once you have implemented your line drawing, you can test it in GHCi with the expression:

    Haskell
    *View> drawingOf (shapeToPicture (Line (3, 3) (5, 7))) 

    There are various example shapes in src/View.hs that you can use to test your code as you go.

  2. Mapping Colours

    Just like the relationship between Shape and Picture, a ColourName (defined in Model.hs) is a type that represents colours abstractly. To use a colour it in a CodeWorld picture, we need to turn it into a value of the CodeWorld Colour type.

    Provide a complete definition for the function colourNameToColour :: ColourName -> Colour in src/View.hs which maps a value of type ColourName into a corresponding Colour value. You will find a number of predefined colours in CodeWorld. For example: black.

    Now provide a definition for the function colourShapeToPicture :: ColourShape -> Picture in src/View.hs, using colourNameToColour. This function takes a pair of type ColourShape and returns a CodeWorld Picture of the colour and shape specified in the input pair. ColourShape is an alias for the tuple type (ColourName, Shape), as defined in Model.hs.

    You can test your implementation of colourShapeToPicture in the GHCi prompt with, for example, the following Haskell expression:

    Haskell
    *View> drawingOf (colourShapeToPicture (Red, Line (3,3) (5,7))) 
  3. Solid Shapes

    Now complete the rest of the shapes, all of which should be solid, as follows.

    • Rectangle to Picture

      Just as with Line, complete the pattern matching for Rectangle in shapeToPicture. To complete the picture for a rectangle you will need to perform some computation. An abstract rectangle is specified by the user giving the coordinates of two of its opposing vertices. However, to create a solid rectangle picture in CodeWorld using the function solidRectangle we are asked to provide only the width and height of the rectangle. If we want to place that rectangle we must use the translated function. So, you will need not only to compute the width and height of the rectangle but also place it appropriately. The maths is not difficult: w=|x1x0| , h=|y1y0| , xc=(x0+x1)/2 , yc=(y0+y1)/2 , where w is the width, h is the height, and xc and yc are the coordinates for the centre of the rectangle.

      Test your code using various rectangles such as squareShape and rectangleShape defined src/View.hs. Make sure that the rectangles appear with the correct position and size.

    • 3.2. Circle to Picture

      Circles are slightly different. The first argument of the constructor Circle records the centre point of the circle, and the second argument records some point on its circumference. CodeWorld’s function circle expects the circle’s radius as its only argument and places the circle in the middle of the screen. Accordingly, you will need to adjust the position of the circle as you did for rectangles.

      Test your code with circle shapes such as circleShape defined in src/View.hs.

    • 3.3. Ellipse to Picture

      Ellipses are defined in Model.hs similarly to rectangles. They have two control points that capture their position, width, and height the same as for rectangles. The question is how to render an ellipse in CodeWorld since it does not define such a picture. Hint: consider that an ellipse is simply a stretched circle, and then observe that CodeWorld has the useful function scaled, which should do the trick.

      Test some ellipses, such as ellipseShape as defined in src/View.hs

    • Polygon to Picture

      The control points for a polygon are a sequence of three or more coordinates. You will find a function in the CodeWorld API that will do the trick.

      Test some polygons, such as triangleShape defined in src/View.hs.

Checkpoint: Part A submission

You must complete Part A before the first deadline, as noted above. You will submit your work so far back to GitLab as follows:

  1. Commit all of your changes using git commit:

    ~/comp1100/assignment1$ git commit -a -m "Submitting Part A of Assignment 1" 
  2. Push your commits back to your remote GitLab repo using git push:

    ~/comp1100/assignment1$ git push 
  3. Make sure that everything is in place by going back and checking that your comp1100-assignment1 project on GitLab at https://gitlab.cecs.anu.edu.au contains all of your work so far.


Part B: Drawing Shapes (Assessable)

The application we are building in this assignment will allow the user to draw multiple shapes on the screen. Now we will first describe essential functions involved in the implementation. The list of tasks you need to complete is given afterwards.

Multiple Shapes

We need a way to capture all of the shapes drawn by the user using the mouse. In the model-view-controller paradigm, the state of a GUI application is held in its model. The model is an abstract representation of the state, while the view is the concrete presentation of that state in graphical form for the user to see. Just as Shape is used to describe a single abstract shape, Model will be used not only to capture a collection of shapes, but also the state of the different tools with which the user can define those shapes.

If you look at the definition for Model in src/Model.hs, you can see that it has three elements: [ColourShape], Tool, and ColourName. The first element is a list of coloured shapes that the user has already created by interacting with the application. The second element is the tool (LineTool, CircleTool, etc.) that the user has selected to draw the next shape. The third element is the colour that the user has selected to draw the next shape. We will use the tool and colour components in Part B.

For now, we just need to be able to render a given Model in graphical form. To do that we have already defined the function modelToPicture in src/View.hs. However, this uses a function colourShapesToPicture that you will complete. See details below.

Drawing

We have just completed the functionality needed to draw abstract shapes on the screen using the CodeWorld API. That is sort of cool and satisfying, but not particularly user-friendly. It would be even more user-friendly to let users interact with the pictures using the mouse to draw shapes, right?

Receiving input from graphical user interface devices (mouse clicks, key presses, etc.) requires what’s called event handling. A program that receives these sorts of inputs is event driven since we cannot prompt the user in advance for a particular item of input. Instead, our programs must wait for particular events to arrive and decide what to do with them. A given event will trigger some action by our program. Thankfully CodeWorld has provided a simple interface function interactionOf, to handle the background tasks (such as capturing the events, and translating them to some nice data types, etc.), leaving us with just the task of assigning actions to various events. That’s a big part of what we are going to be doing in Part B of the assignment.

interactionOf

The function interactionOf that is used in the main function of src/Main.hs is part of the CodeWorld API. Its type signature is:

interactionOf :: world
                -> (Double -> world -> world)
                -> (Event -> world -> world)
                -> (world -> Picture)
                -> IO () 

The type signature mentions the type world. This is not a specific type, but rather a type variable. We’ll get to that later; all we need to know for now is that this type can be any type we want it to be. This type is what we use to track and evolve the state of our world as events occur. The first argument is an initial state for our world when the program first begins (in our case, presumably this will be a Model representing an empty drawing).

The remaining arguments allow us to register event handlers that respond to different events and work with the state of a world (for example, our abstractions of graphics) to transform it accordingly.

These remaining arguments must themselves be functions. They allow us to respond to events in several event domains. The second argument of type Double -> world -> world allows us to register a handler that responds to the passage of time: it will receive a Double and the state of our world, and lets us compute a new state for our world according to the time.

The third argument of type Event -> world -> world handles input events from the user. We will look at the event type shortly, but suffice to say its values are things like key presses and releases, mouse presses and releases, and mouse movements.

The fourth argument is a function that renders the state of our world as a CodeWorld Picture, so that what you see on screen can change over time according to our world model.

Let us try to use this function in a simple way. Open the file src/Controller.hs.

In this example, our world is simply a pair of coordinates, with initial state (0,0) . We do nothing for the time domain, but we respond to user input events by computing a new coordinate depending on which of the up, down, right, and left arrow keys have been pressed by the user. We view the state of our world as the string “Hello World!” drawn at the current coordinate.

Functions handleTime and handleEvent represent the way we handle time and user interface events. Make sure you understand what each function does.

You can run this program using the command cabal run Main. Open the indicated URL in a browser and start typing the up and down arrow keys.

A controller for picture drawing applications

Now, let’s take a look at src/Controller.hs, which is where the action will be in your application.

The state of the world for our picture drawing program will be all of the shapes that the user constructs through the interface. This is precisely what Model type in src/Model.hs describes:

data Model = Model [ColourShape] Tool ColourName deriving (Show) 

This is a custom data type that combines values of three different other types: a list of ColourShapes, a Tool describing which drawing tool the user is currently using to draw with, and ColourName which specifies what colour they are using.

Our initialModel looks like: Model [] (LineTool Nothing) Black, being a world in which there are no shapes, the default initial LineTool is for drawing lines, and the colour to use for drawing is Black. We will arrange for users to switch tools using key press events.

handleTime

Looking back at interactionOf, the first function it asks for needs to have a type signature of Double -> world -> world. Double is the time in seconds since the program started running, and is updated constantly. The world argument to interactionOf represents the current state of the drawing, and the output world allows us to compute a new state at each time step. This is why the function is called handleTime—it handles time. Of course you can call it whatever you want, but handleTime says what it does pretty well. If the state doesn’t vary with time, this function can be a simple identity function that just ignores the time and returns the input state:

handleTime :: Double -> State -> State handleTime = flip const 

If we wanted the state to change with respect to time (in animations for example), you could make more interesting use of the handleTime.

handleEvent

The handleEvent function works in very similar ways, but instead of time in seconds (represented by Double), it defines how the state “reacts” to various events, such as KeyPress, KeyRelease, PointerPress, etc. You will need to us the handleEvent to accept responses from the user, specifically KeyPress, PointerPress, and PointerRelease events.

If the user wants to draw a red line by clicking on the screen at coordinates (1,1) and releasing the mouse at coordinates (2,2) starting at a blank canvas, the state would transition as follows, starting with the initial model:

  1. Model [] (LineTool Nothing) Black

  2. The user presses “C” to change the colour from red to black:

    Model [] (LineTool Nothing) Red

  3. The user clicks the mouse button at (1,1) changing the state to

    Model [] (LineTool (Just (1.0,1.0))) Red

  4. The user releases the mouse button at (2,2) changing the state to

    Model [(Red,Line (1.0,1.0) (2.0,2.0))] (LineTool Nothing) Red

Note that the Tool and the ColourName do not reset to the default values after a shape has been drawn. However, the Maybe Coords inside the tool reverts to Nothing.

modelToPicture

Our application uses modelToPicture as the function to render the updated model as a picture for CodeWorld to display.

Debug Features

If you look at src/Controller.hs, you will see a partially completed handleEvent function. These few lines of code enables you to debug your program more easily. When running the program, and the “D” key is pressed, the program will print the current “State” as a string to the terminal. This gives you complete oversight of the underlying state, so you can make a judgement as to whether your program is doing what you intend it to.

Event Handling

To make this program interactive, you will need to finish the implementation of handleEvent, which is called every time there is some sort of event happening, such as mouse click, key press, etc. With the framework that has been given to you, there is already an example of how to handle KeyPress events:

handleEvent :: Event -> Model -> Model handleEvent event m@(Model ss t c) = case event of KeyPress key
      | k == "Esc" -> initialModel -- revert to an empty canvas | k == "D" -> trace (pack (show m)) m -- the current model on the console | k == "Backspace" || k == "Delete" -> undefined -- TODO: drop the last added shape | k == " " -> undefined -- TODO: finish polygon vertices | k == "T" -> undefined -- TODO: switch tool | k == "C" -> undefined -- TODO: switch colour | otherwise -> m -- ignore other events where k = unpack key PointerPress p -> undefined -- TODO PointerRelease p -> undefined -- TODO _ -> m 

Here are the tasks you should complete for Part B.

  1. Multiple shapes

    Complete colourShapesToPicture.

    We want the first picture in a sequence to be overlaid above the second picture, so make sure you define this function to achieve that properly.

    When you think you are done, test things out using cabal repl View. Try the following in GHCi:

    Haskell
    *View> drawingOf (modelToPicture mystery) 

    Miaow!

  2. Handle the keys (Not assessable)

    Let’s try some rudimentary key handling. Your task is to display the message “Hello World” in the terminal when the “H” key is pressed.

    Please note that the event is captured in the web page but the output will be to the terminal. As such, you will need to run your program, launch a web browser, and press “H” within the blank canvas before checking the terminal to see if the event capture has been successful. The best option is to have your IntelliJ window and your Web browser open side by side.

    After you have achieved this, feel free to remove the “H” key actions from the event handler.

  3. Select the tool and the colour

    Now that you have a basic understanding of event handling involving keys, you can start manipulating the state based on the key presses.

    First make sure that when the canvas is initially drawn, no objects are on it.

    The idea is that the user presses the ‘T’ key to switch tools, and the ‘C’ key to switch colours. For example, a given sequence of interactions would change the model as follows:

    • Initial state: Model [] (LineTool Nothing) Black
    • Key press “T”: Model [] (PolygonTool []) Black
    • Key press “T”: Model [] (RectangleTool Nothing) Black
    • Key press “C”: Model [] (RectangleTool Nothing) Red
    • Key press “T”: Model [] (CircleTool Nothing) Red
    • Key press “C”: Model [] (CircleTool Nothing) Orange
    • Key press “T”: Model [] (EllipseTool Nothing) Orange
    • Key press “T”: Model [] (LineTool Nothing) Orange
    • Key press “C”: Model [] (LineTool Nothing) Yellow
    • Key press “C”: Model [] (LineTool Nothing) Green
    • Key press “C”: Model [] (LineTool Nothing) Blue
    • Key press “C”: Model [] (LineTool Nothing) Violet
    • Key press “C”: Model [] (LineTool Nothing) Black

    You can test your state transitions by pressing the “D” key at any time to print out the current model state in your Terminal.

    Colour transitions should be as follows:

    Previous Next
    Black Red
    Red Orange
    Orange Yellow
    Yellow Green
    Green Blue
    Blue Violet
    Violet Black

    A user can change colour at any time, so the “C” key is always enabled, regardless of the state of the model.

    Tool transitions should be as in the following table:

    Previous Next
    LineTool Nothing PolygonTool []
    PolygonTool [] RectangleTool Nothing
    RectangleTool Nothing CircleTool Nothing
    CircleTool Nothing EllipseTool Nothing
    EllipseTool Nothing LineTool Nothing

    There should be no transition until the user completes use of any given tool. A tool is in use if it is not in one of the given states in the above table. Thus, the “T” key should have no effect when the tool state is not one of the above.

    The user can select a colour first then select a shape, or the other way around. The order of such operations should not matter in your program. This is a strong advantage of event-based input interfaces as opposed to text-based prompts.

  4. Handle the mouse clicks

    In order to draw a rectangle (two of the sides of which are parallel to the x axis, the other two parallel to the y axis), you will need the coordinates of two opposing corners. The user will specify the corners by clicking and dragging. The point at which the user first presses the mouse button is the first corner, and the point at which the user releases the mouse button is the second other.

    As such, there are two mouse Events patterns that you need to match: PointerPress and PointerRelease:

    You can extract the PointerPress Point field the same way that you have done with KeyPress using pattern matching.

    In order to have a feel of what you should do to handle mouse clicks, make your program such that every time there is a click, you print out the coordinates for the click in the terminal using trace. After you have played around with PointerPress and PointerRelease events, it’s time to incorporate them into your program.

    Once a PointerPress event has been delivered to your handleEvent function, you will fill the Maybe type in the particular Tool with the starting coordinates. Once a PointerRelease event has been registered, you will need to use the current model (including the tool) and the mouse point to construct the appropriate shape, for lines, circles, rectangles, and ellipses. For a polygon you will gather three or more mouse clicks, before allowing the user to press the space bar (generating key press event " ") to end the sequence of control points for the polygon. As such, you can ignore PointerRelease events for the polygon tool.

    The following sections give the details for each shape.

  5. PointerRelease for non-polygons

    The user interaction to construct non-polygons (lines, circles, rectangles, ellipses) is click-drag-release:

    i. The user presses the mouse button with the mouse cursor at some point on the screen. You handle this event by updating the current tool in the model to hold that point.

    ii. While still holding the mouse button down, the user drags the mouse cursor to another point on the screen.

    iii. The user releases the mouse button. You handle this event by constructing the appropriate shape corresponding to the current tool, and adding that shape to the list of shapes in the model. The shape should also then appear on the screen because you have already implemented the view update functionality in Part A.

    You should implement this trigger in handleEvent. Note that any attempt to switch the tool should be ignored until the user has completed use of the tool.

    After implementing the PointerRelease trigger, you should be able to draw lines, rectangles, circles, and ellipses on the canvas.

  6. Polygons

    Polygons are a bit trickier to handle. The shape is determined by all of its vertices, so the user needs a way to specify a sequence of vertices. To achieve this, the user:

    i. Selects the polygon tool using the “T” key, and a colour using the “C” key, as described above.

    ii. Clicks multiple control points on the screen. Each click specifies a single vertex that you must record in the polygon tool in the model.

    iii. Presses the “Space” key to indicate that all the vertices have been specified. This key corresponds to the key press event for " " in CodeWorld. You will construct the corresponding polygon shape at this point and add it to the model, whereupon it should appear displayed on the screen (assuming you implemented polygons in Part A).

    Thus for polygons, as mentioned earlier, you should ignore PointerRelease events while the polygon tool is active, and only handle " " key press events after at least three vertices have been added using the polygon tool. Each time there is a PointerPress event, you will append the current coordinates to the list inside the Polygon type.

    For example, the state should change like this:

    • Initial state: Model [] (LineTool Nothing) Black
    • Key press “T”: Model [] (PolygonTool []) Black
    • Key press “C”: Model [] (PolygonTool []) Red
    • Mouse press at (3, 5): Model [] (PolygonTool [(3.0,5.0)]) Red
    • Mouse press at (6, 8): Model [] (PolygonTool [(6.0,8.0),(3.0,5.0)]) Red
    • Mouse press at (9, 2): Model [] (PolygonTool [(9.0,2.0),(6.0,8.0),(3.0,5.0)]) Red
    • Key press “ “ (space bar): Model [(Polygon [(9.0,2.0),(6.0,8.0),(3.0,5.0)])] (PolygonTool []) Red
  7. Remove stuff

    Make it so that when the “Backspace” key is pressed, the most recent shape is removed from the drawing.

Get rid of warnings

If you execute the commands cabal clean and cabal build in the Terminal, there should be no warnings. If there are, read them and fix the problems.


Report

You should write a concise technical report explaining your design choices in completing your program. The maximum word count is 1000. This is not a required word count—concise presentation is a virtue.

Your report must be in PDF format, located at the root of your assignment repository on GitLab and named Report.pdf. Otherwise, it may not be marked.

The report should have a title page with the following items:

  • Your name
  • Your laboratory time and tutor
  • Your university ID
  • Collaborators, if any.

Content

Keep in mind that your audience is your tutors and the lecturers, who are proficient at programming and understand most programming concepts. Therefore, for example, describing how recursion works in your report is not necessary. After reading a technical report, the reader should be comprehensively aware of what problem the program is trying to solve, the reasons for major design choices in the program, as well as how it was tested.

Below is a list of questions that you might consider discussing in your report:

  • What are the objectives of your functions?
  • What were the conceptual or technical issues that you encountered whilst doing the assignment, and how did you get past them?
  • What were the assumptions that you had to make during the assignment?
  • What would you have done differently if you were to do it again?
  • What are the major design choices you have made in your program?
  • Why have you made those choices?
  • How would you make your program better?
  • How did you test your program? What were the results?
  • Which parts of your program might be confusing for the reader? Explain them if so.

Aside from what should be in your report, here are some things that should definitely not be in it: