CS40 Project 3: Interactive CAD

Due 11:59pm Tuesday 23 September 2014
You may work with one partner on this assignment. For this lab, you will extend Project 02 to make your objects more interactive. The basic idea is to add functionality to your code to handle mouse events so that users can add shapes, remove shapes, and move shapes interactively with the mouse. You will need to use some basic linear algebra and work with QT's event handlers mousePressEvent and mouseMoveEvent.

Table of Contents

Getting started
Since you are extending project2, you may continue to use the same git repo as last week. If for some reason, you are changing partner, you may create a separate projects/03 folder using the setup40 script and copying over your files from projects/02
#only do this if you are changing partners
[~]$ cd ~/cs40/projects/02
[02]$ cp * ../03
[02]$ cd ..
[projects]$ vim CMakeLists.txt
#add_subdirectory(03)
Header updates
There are a few minor edits to drawable.h, triangle.h, main.cpp, and mainwindow.ui that I would like you copy over.
[~]$ cd ~/cs40/projects/02
[02]$ cp ~adanner/public/cs40/projects/02/mainwindow.ui ./

To drawable.h add the following methods
/* return true if shape contains point pt, false otherwise
  * Pure virtual. Must be implemented in derived class
  */
virtual bool contains(const vec2& pt) = 0;

/* Hide shape so that it is not drawn */
inline void hide(){ m_visible=false; }

/* return true if visibile, false otherwise */
inline bool isVisible(){ return m_visible; }
Add the private member variable:
bool m_visible;
and change the constructor in drawable.h to
//default, empty constructor
Drawable(): m_color(0.,0.,0.,1.), m_vbo(NULL), m_visible(true) {};
To each of
triangle.h
rectangle.h
circle.h 
line.h
add the method
bool contains(const vec2& pt);
Note that since this method is pure virtual in the drawable.h base class, you will need to add at least a dummy implementation if this method to each corresponding .cpp file before the code will compile, e.g.,
bool Triangle::contains(const vec2& pt) { return false; }

Frames in OpenGL
To change frames, you will set up a matrix transformation in QT/OpenGL. You will use a 4x4 matrix to represent the coordinate transform from screen/mouse coordinates to OpenGL GLSL clip coordinates. On the CPU/QT side, the matrix type is QMatrix4x4. On the GPU/GLSL the type is mat4

To use a matrix transform in your vertex shader, follow the steps below.

First modify your vertex shader vshader.glsl to take a uniform mat4 as input.

#version 130

uniform mat4 mview;
in vec4 vPosition;

void main()
{
    vec4 pos = vPosition;
    pos.w=1.;
    gl_Position =  mview*pos;
}
The pos.w=1; line explicitly sets the last component of the four-tuple to 1., indicating that pos is a point.

Next, add a private member variable QMatrix4x4 m_matrix; to your MyPanelOpenGL header file. Connect your matrix to the shader in paintGL()

m_shaderProgram->setUniformValue("mview", m_matrix);
Everything should compile and run at this point with no visible changes.

You can quickly test that your matrix is working by adding

m_matrix(0,0)=0.5;
m_matrix(1,1)=0.5;
ahead of the call to setUniformValue. The scene should shrink to half size. If this test works, you can remove the two scaling lines.

A real transformation

Recall that the real matrix transformation we want to convert from screen coordinates to clip coordinates is $$ \left[ {\begin{array}{cccr} 2/w & 0 & 0 & -1 \\ 0 & -2/h & 0 & 1 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array} } \right]$$ The first and second columns will change whenever the GL window resizes. In the MyPanelOpenGL constructor add
m_matrix.setColumn(3,QVector4D(-1,1,0,1)); 
In resizeGL(int w, int h) add
m_matrix.setColumn(0,QVector4D(2./w,0,0,0));
m_matrix.setColumn(1,QVector4D(0,-2./h,0,0));
updateGL();

At this point, all previous shapes declared will have their vertices premultiplied by m_matrix, likely resulting in nothing showing up. Add a new shape specified in screen coordinates and verify that this shape appears on the screen

mousePressEvent

To capture clicks from the user, add the method void mousePressEvent(QMouseEvent *event); to the list of protected methods in the MyPanelOpenGL header file. A sample implementation might start like this
void MyPanelOpenGL::mousePressEvent(QMouseEvent *event){
    vec2 click(event->localPos().x(),event->localPos().y());
    QVector4D worldPoint(click, 0, 1);
    QVector4D clipPoint=m_matrix*worldPoint;
    cout << clipPoint.toVector2D() << endl;
}
Note: To use QVector2D with cout, you will need to overload the << operator. I put the following in my geomfun.h
/* helpful debugging ostream operators */
std::ostream& operator<<(std::ostream& str, const QVector2D& vec);
std::ostream& operator<<(std::ostream& str, const QVector4D& vec);
The corresponding implementation in geomfun.cpp would be
/* helpful debugging ostream operators */
std::ostream& operator<<(std::ostream& str, const QVector2D& vec){
    str << (float) vec.x() << ", " << (float) vec.y();
    return str;
}

std::ostream& operator<<(std::ostream& str, const QVector4D& vec){
    str << (float) vec.x() << ", " << (float) vec.y() << ", ";
    str << (float) vec.z() << ", " << (float) vec.w();
    return str;
}

Verify that your mouse handler is working by clicking on points in the GL canvas. You should see the coordinate output of your clicks in clip coordinates. Does this match your expectations? If so, proceed with the rest of the lab.
Drawing Modes
Your program should support the following modes: Adding objects (with a minimal support of lines, triangles, rectangles, and circles), Deleting objects, Moving objects, and changing colors. The QT UI has been modified to include radio buttons for these states. To keep track of which mode is set via the radio buttons, I recommend using a enumerated type to store the mode in a sane way that is easy to remember.
/* define a new type app_mode_t that stores the current mode*/
typedef enum CS40_APP_MODE{
  NONE=0,
  MOVING,
  DELETING,
  CHANGE_COLOR,
  ADD_CIRCLE
} app_mode_t;

app_mode_t m_currentMode=ADD_CIRCLE;
You will probably want to add a private member variable m_currentMode which tracks the current mode. By default, Dragging is set in the radio box, so you may initialize m_currentMode to MOVING in the OpenGL panel constructor. You will want to go into the UI slots/signals editor and drag clicked() signals from each radio button in the mode box to the GLWidget panel, and connect each signal to an appropriate slot, e.g., void modeDrag(). Note you may need to manually add the slot in the slot/signal editor for the GLWidget class. For each slot, add the appropriate method name under the public slots: section in mypanelopengl.h and implement the method in mypanelopengl.cpp. The implementation could be something as simple as
void MyPanelOpenGL::modeMoving(){ m_currentMode=MOVING; }
Setting Color state
Another handy state variable in mypanelopengl.h would be the active color as indicated by the radio button. You can store this as a QVector3D object. Add additional slots to change the color. Connect e.g., the Red radio button clicked() signal to a setRed() slot in the OpenGL panel. An implementation might be:
void MyPanelOpenGL::setRed(){ curr_color=vec3(1.,0,0); }
The random color could be truly random, or something non-random (arbitrary) of your choosing.
Adding shapes
If a user is in an Adding shapes mode, left mouse clicks should add shapes to the screen. For example, if the user is in add circle mode, the first mouse click will set the center of the circle and the second mouse click will define a point on the perimeter of the circle. At this point, your code should create a Circle object using your project 02 code, add it to a QList of current objects on the screen and draw the object using the current color. A similar pattern should be followed for other shapes. In the case of triangles and rectangles, your must orient your shapes in a counter clockwise direction regardless of how the user clicks on the points. Your constructors should arrange the points to achieve the correct orientation. After creating the shape, you can call updateGL() to trigger paintGL() and redraw the scene.
Moving shapes
In moving shapes mode, a mouse click should determine if the click is within a shape and if so, the shape containing the point can be moved by dragging the mouse to a new location for the shape. To track the mouse, you can add a mouseMoveEvent method that is triggered when the mouse is moving across the display and the mouse button is pressed. If multiple shapes overlap and contain the mouse pointer click, your code should move the object that was most recently added to the screen, which should appear on top.

You shape should interactively follow the mouse while you are dragging the shape and stop moving only when you release the mouse button or the mouse goes outside the openGL window.

To implement this correctly, you will need to check if a point is inside a shape. The Drawable method virtual bool contains(const vec2& pt)=0; must be implemented appropriately in each of the derived classes. You should implement leftOf and distToSegment functions in your geomfun.h files as mentioned in project2. With these helper methods, writing the inside method for Line and Triangle should be easier. Note that the QVector2D class has a built in length method which may be helpful at times. You can also directly add/subtract QVector2D objects, e.g., myvec = pt1-pt2; It is very difficult to click inside a line. For this project, implement contains for a line by checking if the minimum distance from the user click to the line segment is within an acceptable tolerance. You can choose a tolerance that works best for your application.

Deleting Shapes
In Delete mode, clicking on a shape should remove it from the screen. You do not need to actually call delete on the actual Drawable* corresponding to the object. Just set a flag in the object indicating that the object should not be drawn. This is typically called lazy deletions where we just mark things as deleted and actually delete them later. If you are obsessed with cleaning, you may occasionally scan the vector, removing hidden objects, freeing memory, and repacking the vector, but this is a fair amount of unnecessary work at this point. You should however free all the dynamically allocated memory before the program exits. The new helper methods hide and isVisible in the Drawable class are helpful here. Remember to modify your shape constructors (both the default and copy constructors) to set a default value for m_visible. Modify draw to simply return without drawing if the shape is not visible. When selecting items for moving, dragging, or changing colors, do not select objects which are currently invisible.
Changing colors
In change color mode, clicking on a shape should change its color. This is fairly easy once you have the slots set up and the contains method for each shape implemented properly. Don't forget to call updateGL() to ensure the changes take effect.
Coordinate Transformation
When you click on the window, the coordinates passed to the mousePressEvent or mouseMoveEvent in QT are in screen coordinates (think pixels), not world coordinates (where the shapes live), nor clip coordinates (which determines what OpenGL draws). The objects you draw should be in world coordinates. You must write a function that constructs a 4x4 matrix which converts from screen coordinates to clip coordinates. To further complicate things, the screen is left handed, meaning that x increases from left to right, but y increases from top to bottom. Clip coordinates are right handed, and y increases from bottom to top. The matrix you build should be passed on to the vertex shader a uniform mat4 value. You will need to reconstruct the matrix only after reshape events. Furthermore, since all shapes use the same matrix to convert, you can set the value once in paintGL, or maybe even reshapeGL, instead of setting it in every draw() method.
Testing
Test that all the radio button features work as expected.
Submit
You should regularly commit your changes and occasionally push to your shared remote. Note you must push to your remote to share updates with your partner. Ideally you should commit changes at the end of every session of working on the project. You will be graded on work that appears in your remote by the project deadline.
Bonus: Pan/Zoom
This section is entirely optional, but I will offer a maximum of one bonus point (out of ten) if you implement panning and/or zooming. Do not attempt this unless the required features are working.

The idea behind panning and zooming is that you will be working with three coordinate frames: 1) the window frame used by mouse events 2) the world frame used by the coordinates of the shapes, and 3) the clip frame used by the fragment shader. You will need one matrix to transform from window coordinates to world coordinates, and a second matrix to transform from world to clip coordinates.

Start by representing the bounding box of visible scene in world coordinates using a struct.

typedef struct {
  float xmin, xmax, ymin, ymax;
} boundingBox;

boundingBox worldBox;
You can choose the initial size of your world. Initialize the mouse to world matrix to transform from the OpenGL window size in mouse coordinates, to coordinate in the world frame. Note you will need to update this matrix if either the window resizes or you pan/zoom the world.

Initialize the world to clip matrix to map the current worldBox onto the clip square from (-1,-1) to (1,1). Pass only this matrix as a uniform to your vertex shader. By storing a global bounding box representing the projection, it is fairly easy to add support of zooming in, zooming out, and panning around the display. Add support to your code to allow users to zoom in and out using the 'z'/'Z' keys, respectively and to pan around the scene using up, down, left and right arrow keys. You may find the keyPressEvent method helpful.

Alternatively, you could add QT widgets to your UI to allow zooming panning with the mouse. A zoom or pan would update the bounding box of your current view of the world and require updates to the change of frame matrices. Modify as needed. When adding a shape, convert mouse clicks to world coordinates and add you shapes using world coordinates, not mouse coordinates.