Elysian Shadows

Terrain Selection Flip and Rotation

For the time being, you may use the feature as follows:

[color=#BF40FF]Flip Horizontal[/color]: Up Key

[color=#BF40FF]Flip Vertical[/color]: Down Key

[color=#BF40FF]Rotate 90 Degrees Clockwise[/color]: Right Key

[color=#BF40FF]Rotate 90 Degrees Counterclockwise[/color]: Left Key

It is my hope that James and Jarrod will take my back-end design and work their front-end magic to present the user with the most intuitive experience. ;)

[b][color=#BF40FF]Getting Rid of the Hashmap[/b][/color]

One of the poorer design decisions of the Toolkit was our usage of a QHashMap to store the tile selections. This was utilized to give us O(1) lookup time, but also introduced quite a bit of memory overhead and quite a bit of processing overhead due to the serialization and deserialization of the hash key. This also prevented us from iterating through the selection tiles in any particular order (this is just the nature of a hashmap). 

As it turns out, the optimal solution was to use a 2D array to store these values. The array is directly indexable with the x and y map locations of the selection, contains no wasted space, and is a perfect O(1) lookup time without any serialization overhead. 

When a selection is made (either in MapView or SceneView), an instance of TerrainSelection is instantiated. This instance allocates two dynamic 2D arrays to hold selection data. The first is the actual tile values (uint16s). The second is an array of booleans for whether the selection is empty or not. This is required for Marcel's "staggered selection" to work, because not every tile within the 2D array's rectangular region is technically selected.

This method took quite awhile to get perfectly stable, but now that it is, I have no doubt that it is superior to the previous in every way possible.

[b][color=#BF40FF]Refactoring SelectionView[/b][/color]

The entire "Selection View" hierarchy was a reesting mess previously. "TileSelectionView" was initially the parent class of any QGraphicsView implementing our selection mechanism. This was subclassed by "SceneView" and "SelectionView." Unfortunately, the parent was directly dependent upon items within the SceneView, and required inconsistent virtual member overriding to work correctly for SelectionView and other viewsĂ–

First of all, I fixed the reested naming. The parent class is now "SelectionView." It is now subclassed by "MapView" and "SheetView" to stay consistent with our UI. It now operates upon generic "SelectionViewTerrainItems" rather than items from any specific view. MapView and SheetView are then populated by items that inherit from this generic parent item ("MapViewTerrainItem" and "SheetViewTerrainItem") and implement any additional functionality required. 

The Selection System's internals are now perfectly object-oriented, and their names logically correspond to the Toolkit's UI.

[b][color=#BF40FF]Texture Orientation Combinations[/b][/color]

The biggest hurdle presented by the entire feature was the number of different combinations of tile orientations that simple flipping and 90 degree rotation could produce:

[color=#BF40FF][b]LibGyro Enumeration and Driver Back-End[/b][/color]

I took each combination and created an enumeration within LibGyro. 

/Every possible way to orient a texture's UV coords (3 bits) typedef enum _GY_RECT_TEX_ORIENT { GY_RECT_TEX_ORIENT_NORMAL, GY_RECT_TEX_ORIENT_FLIP_HORIZ, GY_RECT_TEX_ORIENT_FLIP_VERT, 	GY_RECT_TEX_ORIENT_ROT_90, GY_RECT_TEX_ORIENT_ROT_180, GY_RECT_TEX_ORIENT_ROT_270,  	GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_90, GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_270, 	GY_RECT_TEX_ORIENT_MAX //not representable with 3 bits } GY_RECT_TEX_ORIENT; 

LibGyro's Rect API provides a function that will orient the texture of the next rectangle submitted using this enumeration:

void gyVidRectTexOrient(const GY_RECT_TEX_ORIENT orient);

You will notice that there is a grand total of 8 different orientations. That's perfect for us, because that only requires 3 additional bits of data for each tile. So now every tile will be represented with 2 bytes. The low byte is the same 0-255 tile index. The next 3 bits correspond to the LibGyro texture orientation enumeration. 

[b][color=#BF40FF]Terrain Selection Group Rotation[/b][/color]

Before we can actually reorient anything, it is important to understand that there are two layers of complexity to this issue. Since the Toolkit allows group selections, the entire group must be reoriented ALONG WITH each individual tile. These two sets of reorientation algorithms are implemented largely independently of each other.

The high-level Flip and Rotation algorithms are rather straight-forward an operate upon the 2D arrays:

Horizontal Flip:

void TerrainSelection::FlipVertical(void) { 	for(unsigned x = 0; x < _width; ++x) { 		for(unsigned y = 0; y <= _height/2; ++y) { 			uint16_t temp = ReorientTerrainValue(_terrain[y][x], TEX_REORIENT_FLIP_VERT);//_terrain[y][x] | (GY_RECT_TEX_ORIENT_FLIP_HORIZ<<8); 			_terrain[y][x] = ReorientTerrainValue(_terrain[_height-y-1][x], TEX_REORIENT_FLIP_VERT);//_terrain[y][_width-x-1] | (GY_RECT_TEX_ORIENT_FLIP_HORIZ<<8); 			_terrain[_height-y-1][x] = temp; 			bool temb = _inSelection[y][x]; 			_inSelection[y][x] = _inSelection[_height-y-1][x]; 			_inSelection[_height-y-1][x] = temb; 		} 	} } 

Counterclockwise Rotation:

void TerrainSelection::RotateCC(void) { 	uint16_t **nTerrain; 	bool **nSelected; 	unsigned nWidth = _height; unsigned nHeight = _width; 	AllocArrays(nTerrain, nSelected, nWidth, nHeight); 	for(unsigned y = 0; y < nHeight; ++y) { 		for(unsigned x = 0; x < nWidth; ++x) { 			nTerrain[y][x] = ReorientTerrainValue(_terrain[x][_width-y-1], TEX_REORIENT_ROT_CC); 			nSelected[y][x] = _inSelection[x][_width-y-1]; 		} 	} 	DeleteArrays(_terrain, _inSelection, _width, _height); 	_width = nWidth; 	_height = nHeight; 	_terrain = nTerrain; 	_inSelection = nSelected; } 

You should note that since the actual dimensions of the 2D array change with rotations, the arrays have to be reallocated with different sizes. This is why I have written the static Allocate/Delete helper functions.

[b][color=#BF40FF]Terrain Orientation Lookup Table[/b][/color]

Before we get into orienting individual tiles, we need to address another important issue. Lets say that we flip a tile horizontally and place it on the map. Then lets say that we select this tile along with 10 other surrounding tiles in a group selection. Then lets say that we rotate the entire group to 90 degrees. While every tile will now be oriented by 90 degrees, this starting tile has a horizontal flip AND a 90 degree rotation applied to itĂ– 

For this reason, I had to devise a scheme of orienting already oriented tiles. The solution came to me as I was belligerently drunk one night: A static constant 2D array lookup table.

The first dimension of this table is the current orientation. The second dimension is the orientation that we wish to apply to it. The result is the new tile orientation:

//8x4 Lookup table for reorienting oriented textures. Flexing my devmeat! const GY_RECT_TEX_ORIENT TerrainSelection::_texOrientLut[GY_RECT_TEX_ORIENT_MAX][TEX_REORIENT_MAX] = { 	{ GY_RECT_TEX_ORIENT_FLIP_HORIZ, GY_RECT_TEX_ORIENT_FLIP_VERT, GY_RECT_TEX_ORIENT_ROT_270, GY_RECT_TEX_ORIENT_ROT_90 }, 	{ GY_RECT_TEX_ORIENT_NORMAL, GY_RECT_TEX_ORIENT_ROT_180, GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_90, GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_270 }, 	{ GY_RECT_TEX_ORIENT_ROT_180, GY_RECT_TEX_ORIENT_NORMAL, GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_270, GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_90 }, 	{ GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_90, GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_270, GY_RECT_TEX_ORIENT_NORMAL, GY_RECT_TEX_ORIENT_ROT_180 }, 	{ GY_RECT_TEX_ORIENT_FLIP_VERT, GY_RECT_TEX_ORIENT_FLIP_HORIZ, GY_RECT_TEX_ORIENT_ROT_90, GY_RECT_TEX_ORIENT_ROT_270 }, 	{ GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_270, GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_90, GY_RECT_TEX_ORIENT_ROT_180, GY_RECT_TEX_ORIENT_NORMAL }, 	{ GY_RECT_TEX_ORIENT_ROT_90, GY_RECT_TEX_ORIENT_ROT_270, GY_RECT_TEX_ORIENT_FLIP_VERT, GY_RECT_TEX_ORIENT_FLIP_HORIZ }, 	{ GY_RECT_TEX_ORIENT_ROT_270, GY_RECT_TEX_ORIENT_ROT_90, GY_RECT_TEX_ORIENT_FLIP_HORIZ, GY_RECT_TEX_ORIENT_FLIP_VERT } }; 

Using this lookup table, the implementation of "ReorientTerrainValue" is trivial:

uint16_t TerrainSelection::ReorientTerrainValue(const uint16_t terrain, const TEX_REORIENT orient) { 	GY_RECT_TEX_ORIENT curOrient = (GY_RECT_TEX_ORIENT)((terrain>>8) & 0x0007); //extract orientation bits 	uint16_t newTerrain = (terrain&0x00ff); //clear the high byte 	return newTerrain |= (_texOrientLut[curOrient][orient]<<8); //Append new orient bits } 

We take the current tile value, extract its orientation, then use this orientation and the applied orientation to derive its new orientation. This new orientation is then packed back with the tile index. Boom, bitches. :D

[b][color=#BF40FF]Rendering[/b][/color]

With this mechanism, rendering each MapViewTerrainItem is also trivial. In its render function, it simply checks its own orientation and reorients its texture:

void MapViewTerrainItem::RenderFlipRotPixmap(QPainter *const painter, QPixmap &pixmap, GY_RECT_TEX_ORIENT orient) { 	switch(orient) { 		case GY_RECT_TEX_ORIENT_NORMAL: 			painter->drawPixmap(-16, -16, 32, 32, pixmap); 			break; 		case GY_RECT_TEX_ORIENT_FLIP_HORIZ: 			painter->scale(-1.0, 1.0); 			painter->drawPixmap(-16, -16, 32, 32, pixmap); 			painter->scale(1.0, 1.0); 			break; 		case GY_RECT_TEX_ORIENT_FLIP_VERT: 			painter->scale(1.0, -1.0); 			painter->drawPixmap(-16, -16, 32, 32, pixmap); 			painter->scale(1.0, 1.0); 			break; 		case GY_RECT_TEX_ORIENT_ROT_90: 			painter->rotate(-90.0); 			painter->drawPixmap(-16, -16, 32, 32, pixmap); 			painter->rotate(0.0); 			break; 		case GY_RECT_TEX_ORIENT_ROT_180: 			painter->rotate(180.0); 			painter->drawPixmap(-16, -16, 32, 32, pixmap); 			painter->rotate(0.0); 			break; 		case GY_RECT_TEX_ORIENT_ROT_270: 			painter->rotate(-270.0); 			painter->drawPixmap(-16, -16, 32, 32, pixmap); 			painter->rotate(0.0); 			break; 		case GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_90: 			painter->scale(-1.0, 1.0); 			painter->rotate(-90.0); 			painter->drawPixmap(-16, -16, 32, 32, pixmap); 			painter->rotate(0.0); 			painter->scale(1.0, 1.0); 			break; 		case GY_RECT_TEX_ORIENT_FLIP_HORIZ_ROT_270: 			painter->scale(-1.0, 1.0); 			painter->rotate(-270.0); 			painter->drawPixmap(-16, -16, 32, 32, pixmap); 			painter->rotate(0.0); 			painter->scale(1.0, 1.0); 			break; 	} 	//WHEEEEEEEELL!!!! SCREW THIS!!! 	//Correct our own transformation reest. 	//painter->resetTransform(); } 

edit: You will probably note that the rotations are negative here. That is because the handedness of QT's space is reverse our engine's. Since the negative Y direction is up in the engine, a 90 degree rotation is counterclockwise. It's clockwise in QT. I negate the orientations to compensate.

I hope you guys will leave me feedback on this feature before its officially released in 1.5.2 or 1.6. As always, carry on, my dear dev brothers. You have reignited my flames, so I am going to help push you guys to success. ;)

Falco Girgis
Falco Girgis is the founder and lead software architect of the Elysian Shadows project. He was previously employed in the telecom industry before taking a chance on Kickstarter and quitting his job to live the dream. He is currently pursuing his masters in Computer Engineering with a focus on GPU architecture.