Introduction

Getting Started

Programming Guides

API Reference

Additional Resources

2D Tutorial: Snapping to a Grid

This step reviews how to display a grid and filter selection locations so that they are constrained to grid locations. This is referred to as 'snapping'.

The HSnapGrid class provides support for contraining selection points to a grid, and is provided along with MVO. We'll extend HSoccerView.h to have a pointer and new Get() method to deal with the grid, along with a member and associated utilies to set/get the current snap mode. (Extracted from HSoccerView.h)

#include "HSnapGrid.h"
class HSoccerView : public HBaseView
{
public:
HSnapGrid * GetSnapGrid() {return (m_pSnapGrid);}
bool GetSnap() {return (m_bSnap);}
void SetSnap(bool snap) {m_bSnap = snap;}
protected:
HSnapGrid * m_pSnapGrid;
bool m_bSnap;
};

Constructor

We need to initialize the new members in the HSoccerView constructor:

HSoccerView::HSoccerView(HBaseModel *model,
const char * alias,
const char * driver_type,
const char * instance_name,
void * window_handle,
void * colormap) : HBaseView(model, alias, driver_type, instance_name, window_handle, colormap)
{
m_pSnapGrid = 0;
m_bSnap = false;
}

Creating and Initializing the Grid Object

We'll setup the grid object in HSoccerView::Init(), and create an infinite grid with x-y units of .05

{
HPoint origin, ref1, ref2;
bool success;
origin.Set(0.0, 0.0, 0.0);
ref1.Set(0.05, 0.0, 0.0);
ref2.Set(0.0, 0.05, 0.0);
.
.
.
m_pSnapGrid = new HSnapGrid(this);
success = m_pSnapGrid->Create(&origin, &ref1, &ref2);
.
.
.
}

Placing the Model on Top of the Grid

If you run the program at this stage, you will note that the grid is on top of all the model geometry. This is because we haven't set a specific drawing order for the grid relative to the whole model. All we previously did was perform some reordering (using ::Set_Rendering_Options("depth range = <values>)") of the soccer field 'layer' segments, which were all subsegments of the 'model' segment. This previous reordering will do nothing to ensure that the entire model geometry gets drawn on top of the grid. Thus we need to make sure the 'grid geometry segment' is drawn at a depth range that places it 'behind' all the other geometry. Values of .95 and 1.0 would ensure this, and we can add the depth range setting to HSoccerView::Init after creating the grid:

if (success)
{
HC_Open_Segment_By_Key(m_pSnapGrid->GetGridSegment());
HC_Set_Rendering_Options("depth range=(0.95, 1.0)");
HC_Close_Segment();
}

Improving Model Contrast against the Grid

There are few minor visual problems remaining. First, you can see that the soccer-field geometry doesn't contrast well with the grid since the line/edge thickness of both the field and grid geometry are the same. Let's increase the thickiness of the lines/edges in the model. We do so by setting the line/edge weight attributes at the top of the model hierarchy. A value of 2.0 is good (which is double the default weight of 1.0 that the grid geometry is still getting drawn with)

{
.
.
HC_Open_Segment_By_Key(GetModelKey());
HC_Set_Line_Weight(2.0);
HC_Set_Edge_Weight(2.0);
.
.
.
HC_Close_Segment();
}

Then, let's increase the thickness of the temporary construction rectangles and change their color to give them better contrast. If you look at the source to HOpConstructRectangle, you'll note that the rectangles get created in the segment denoted by HBaseView::m_WindowspaceKey. Therefore, in our custom Init() function, we can open up that segment and set a better color and thicker edge/line weight:

{
.
.
HC_Open_Segment_By_Key(m_WindowspaceKey);
HC_Set_Color("edges = lines = red");
HC_Set_Edge_Weight(2.0);
HC_Set_Line_Weight(2.0);
HC_Close_Segment();
.
.
}

A quick note on the difference between 'lines' and 'edges'. In HOOPS/3dGS, 'lines' refer to the visual representationof geometry that has no 'face' component, such as lines, polylines and arcs. 'edges' refer to the borders of geometry that have a 'face' component, such as polygons, circles, shells/meshes, etc... Refer to Sections 2.5 - 2.10 in the HOOPS/3dGS Programming Guide for more information. Since our soccer field scene-graph contains both lines and polygons, we need to set both line and edge weights to get the desired effect.

Filtering Selection Points

HSnapGrid provides various utility functions such as GetSnappedWorldPos() that accept an input point within a particular coordinate system, and return the location of the closest grid intersection in the same coordinate system. We'll use these functions to filter the mouse locations when they arrive at the application GUI-specific event handler methods. In review, our MFC application's main GUI-view object (CSoccerView) is derived from CHoopsView, which is part of HOOPS/MFC. CHoopsView's key/mouse handler methods map the incoming key/mouse events to the current operator's matching event. What we'll do is overload the mouse handler events to filter the point (snapping it to our grid) and then call through to the base CHoopsView methods for regular processing. The following extracts from snap/CSoccerView.h declare the methods that we'll overload.

class CSoccerView : public CHoopsView
{
protected:
//{{AFX_MSG(CSoccerView)
.
.
afx_msg virtual void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg virtual void OnLButtonUp(UINT nFlags, CPoint point);
afx_msg virtual void OnMouseMove(UINT nFlags, CPoint point);
.
.
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};

The implementation of each method is quite similar (the same holds true for any mouse method that we may want to overload). So, we'll take closer look at just one of the methods. CSoccerView::OnLButtonDown gets called when the left mouse button goes down. The incoming point is in 'pixel' space; we'll convert it to 'world' space, filter the point by snapping to our grid (which is in world space), convert back to 'pixel' space, and then call the base with the filtered point. We begin by checking if there is an HBaseView object, and then obtaining a pointer to the HSnapGrid object.

void CSoccerView::OnLButtonDown(UINT nFlags, CPoint point)
{
if (m_pHView)
{
HPoint temp_point;
HSnapGrid * grid = ((HSoccerView *)m_pHView)->GetSnapGrid();

If there is a grid and we actually want to snap to it, we then initialize a local HEventInfo structure with the point information. HEventInfo will store the point info in various coordinate systems.

if (grid && ((HSoccerView *)m_pHView)->GetSnap())
{
HEventInfo event(m_pHView);
event.SetPoint(HE_LButtonDown, point.x, point.y, MapFlags(nFlags));

Next, we obtain the coordinate of the point in 'world' space, and then ask the grid object to return the snapped location in 'world' space

temp_point = event.GetMouseWorldPos();
grid->GetSnappedWorldPos(temp_point);

Finally, we convert the snapped coordinate from 'world' space back to 'pixel' space, reset the 'point' structure so that it contains our snapped point, and then call the base class' method to continue with regular processing.

HC_Open_Segment_By_Key (m_pHView->GetSceneKey());
HC_Compute_Coordinates(".", "world", &temp_point, "local pixels", &temp_point);
HC_Close_Segment();
point.SetPoint(temp_point.x, temp_point.y);
}
CHoopsView::OnLButtonDown(nFlags, point);
}
}

Toggling Grid Usage

Finally, we need to enable use of the grid at some point so that GetSnap() returns 'true'! Let's any snap to the grid when the HOpCreateRectangle operator is active. We'll turn it on at the place where we create the new operator in CSoccerView:

{
// turn on snapping for this operator
((HSoccerView *)m_pHView)->SetSnap(true);
}

We'll turn it off in the LocatSetOperator(), using that as a common place to do initialization work when toggling operators.(since we call SetSnap(true) after calling LocalSetOperator per the code above, the snap mode will be turned on for the HOpCreateRectangle operator)

void CSoccerView::LocalSetOperator(HBaseOperator * NewOperator)
{
HBaseOperator * op = GetSolidView()->GetOperator();
if (op)
delete op;
// turn off the snap mode
((HSoccerView *)m_pHView)->SetSnap(false);
GetSolidView()->SetOperator(NewOperator);
}

 

Cleaning Up

Finally, we clean up the grid object in the view's destructor:

{
.
.
H_SAFE_DELETE(m_pSnapGrid);
.
.
}

Go ahead and test our your new snap functionality. You've now built a simple 2D Application that selects/manipulates layers, displays some decent-looking text, and creates a 2d entity-type snapped to a grid. Continuing along these lines, you could further enhance the selection support, add additional custom 2D geometry creation operators, enhance text support by providing a font chooser list populated with the fonts returned from an ::Begin_Font_Search operation, etc...