Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
/*
OLC::CAD - A practical example of Polymorphism
"Damn Gorbette, you made us giggle..." - javidx9
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2018-2019 OneLoneCoder.com
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions or derivations of source code must retain the above
copyright notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce
the above copyright notice. This list of conditions and the following
disclaimer must be reproduced in the documentation and/or other
materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Instructions:
~~~~~~~~~~~~~
Press & Hold middle mouse mutton to PAN
Use Scroll wheel (or Q & A) to zoom in & out
Press L to start drawing a line
Press C to start drawing a circle
Press B to start drawing a box
Press S to start drawing a curve
Press M to move node under cursor
Relevant Video: https://youtu.be/kxKKHKSMGIg
Links
~~~~~
YouTube: https://www.youtube.com/javidx9
https://www.youtube.com/javidx9extra
Discord: https://discord.gg/WhwHUMV
Twitter: https://www.twitter.com/javidx9
Twitch: https://www.twitch.tv/javidx9
GitHub: https://www.github.com/onelonecoder
Patreon: https://www.patreon.com/javidx9
Homepage: https://www.onelonecoder.com
Author
~~~~~~
David Barr, aka javidx9, ©OneLoneCoder 2019
*/
#define OLC_PGE_APPLICATION
#include "olcPixelGameEngine.h"
// Forward declare shape, since we use it in sNode
struct sShape;
// Define a node
struct sNode
{
sShape *parent;
olc::vf2d pos;
};
// Our BASE class, defines the interface for all shapes
struct sShape
{
// Shapes are defined by the placment of nodes
std::vector<sNode> vecNodes;
uint32_t nMaxNodes = 0;
// The colour of the shape
olc::Pixel col = olc::GREEN;
// All shapes share word to screen transformation
// coefficients, so share them staically
static float fWorldScale;
static olc::vf2d vWorldOffset;
// Convert coordinates from World Space --> Screen Space
void WorldToScreen(const olc::vf2d &v, int &nScreenX, int &nScreenY)
{
nScreenX = (int)((v.x - vWorldOffset.x) * fWorldScale);
nScreenY = (int)((v.y - vWorldOffset.y) * fWorldScale);
}
// This is a PURE function, which makes this class abstract. A sub-class
// of this class must provide an implementation of this function by
// overriding it
virtual void DrawYourself(olc::PixelGameEngine *pge) = 0;
// Shapes are defined by nodes, the shape is responsible
// for issuing nodes that get placed by the user. The shape may
// change depending on how many nodes have been placed. Once the
// maximum number of nodes for a shape have been placed, it returns
// nullptr
sNode* GetNextNode(const olc::vf2d &p)
{
if (vecNodes.size() == nMaxNodes)
return nullptr; // Shape is complete so no new nodes to be issued
// else create new node and add to shapes node vector
sNode n;
n.parent = this;
n.pos = p;
vecNodes.push_back(n);
// Beware! - This normally is bad! But see sub classes
return &vecNodes[vecNodes.size() - 1];
}
// Test to see if supplied coordinate exists at same location
// as any of the nodes for this shape. Return a pointer to that
// node if it does
sNode* HitNode(olc::vf2d &p)
{
for (auto &n : vecNodes)
{
if ((p - n.pos).mag() < 0.01f)
return &n;
}
return nullptr;
}
// Draw all of the nodes that define this shape so far
void DrawNodes(olc::PixelGameEngine *pge)
{
for (auto &n : vecNodes)
{
int sx, sy;
WorldToScreen(n.pos, sx, sy);
pge->FillCircle(sx, sy, 2, olc::RED);
}
}
};
// We must provide an implementation of our static variables
float sShape::fWorldScale = 1.0f;
olc::vf2d sShape::vWorldOffset = { 0,0 };
// LINE sub class, inherits from sShape
struct sLine : public sShape
{
sLine()
{
nMaxNodes = 2;
vecNodes.reserve(nMaxNodes); // We're gonna be getting pointers to vector elements
// though we have defined already how much capacity our vector will have. This makes
// it safe to do this as we know the vector will not be maniupulated as we add nodes
// to it. Is this bad practice? Possibly, but as with all thing programming, if you
// know what you are doing, it's ok :D
}
// Implements custom DrawYourself Function, meaning the shape
// is no longer abstract
void DrawYourself(olc::PixelGameEngine *pge) override
{
int sx, sy, ex, ey;
WorldToScreen(vecNodes[0].pos, sx, sy);
WorldToScreen(vecNodes[1].pos, ex, ey);
pge->DrawLine(sx, sy, ex, ey, col);
}
};
// BOX
struct sBox : public sShape
{
sBox()
{
nMaxNodes = 2;
vecNodes.reserve(nMaxNodes);
}
void DrawYourself(olc::PixelGameEngine *pge) override
{
int sx, sy, ex, ey;
WorldToScreen(vecNodes[0].pos, sx, sy);
WorldToScreen(vecNodes[1].pos, ex, ey);
pge->DrawRect(sx, sy, ex - sx, ey - sy, col);
}
};
// CIRCLE
struct sCircle : public sShape
{
sCircle()
{
nMaxNodes = 2;
vecNodes.reserve(nMaxNodes);
}
void DrawYourself(olc::PixelGameEngine *pge) override
{
float fRadius = (vecNodes[0].pos - vecNodes[1].pos).mag();
int sx, sy, ex, ey;
WorldToScreen(vecNodes[0].pos, sx, sy);
WorldToScreen(vecNodes[1].pos, ex, ey);
pge->DrawLine(sx, sy, ex, ey, col, 0xFF00FF00);
// Note the radius is also scaled so it is drawn appropriately
pge->DrawCircle(sx, sy, (int32_t)(fRadius * fWorldScale), col);
}
};
// BEZIER SPLINE - requires 3 nodes to be defined fully
struct sCurve : public sShape
{
sCurve()
{
nMaxNodes = 3;
vecNodes.reserve(nMaxNodes);
}
void DrawYourself(olc::PixelGameEngine *pge) override
{
int sx, sy, ex, ey;
if (vecNodes.size() < 3)
{
// Can only draw line from first to second
WorldToScreen(vecNodes[0].pos, sx, sy);
WorldToScreen(vecNodes[1].pos, ex, ey);
pge->DrawLine(sx, sy, ex, ey, col, 0xFF00FF00);
}
if (vecNodes.size() == 3)
{
// Can draw line from first to second
WorldToScreen(vecNodes[0].pos, sx, sy);
WorldToScreen(vecNodes[1].pos, ex, ey);
pge->DrawLine(sx, sy, ex, ey, col, 0xFF00FF00);
// Can draw second structural line
WorldToScreen(vecNodes[1].pos, sx, sy);
WorldToScreen(vecNodes[2].pos, ex, ey);
pge->DrawLine(sx, sy, ex, ey, col, 0xFF00FF00);
// And bezier curve
olc::vf2d op = vecNodes[0].pos;
olc::vf2d np = op;
for (float t = 0; t < 1.0f; t += 0.01f)
{
np = (1 - t)*(1 - t)*vecNodes[0].pos + 2 * (1 - t)*t*vecNodes[1].pos + t * t * vecNodes[2].pos;
WorldToScreen(op, sx, sy);
WorldToScreen(np, ex, ey);
pge->DrawLine(sx, sy, ex, ey, col);
op = np;
}
}
}
};
// APPLICATION STARTS HERE
class Polymorphism : public olc::PixelGameEngine
{
public:
Polymorphism()
{
sAppName = "Polymorphism";
}
private:
// Pan & Zoom variables
olc::vf2d vOffset = { 0.0f, 0.0f };
olc::vf2d vStartPan = { 0.0f, 0.0f };
float fScale = 10.0f;
float fGrid = 1.0f;
// Convert coordinates from World Space --> Screen Space
void WorldToScreen(const olc::vf2d &v, int &nScreenX, int &nScreenY)
{
nScreenX = (int)((v.x - vOffset.x) * fScale);
nScreenY = (int)((v.y - vOffset.y) * fScale);
}
// Convert coordinates from Screen Space --> World Space
void ScreenToWorld(int nScreenX, int nScreenY, olc::vf2d &v)
{
v.x = (float)(nScreenX) / fScale + vOffset.x;
v.y = (float)(nScreenY) / fScale + vOffset.y;
}
// A pointer to a shape that is currently being defined
// by the placment of nodes
sShape* tempShape = nullptr;
// A list of pointers to all shapes which have been drawn
// so far
std::list<sShape*> listShapes;
// A pointer to a node that is currently selected. Selected
// nodes follow the mouse cursor
sNode *selectedNode = nullptr;
// "Snapped" mouse location
olc::vf2d vCursor = { 0, 0 };
// NOTE! No direct instances of lines, circles, boxes or curves,
// the application is only aware of the existence of shapes!
// THIS IS THE POWER OF POLYMORPHISM!
public:
bool OnUserCreate() override
{
// Configure world space (0,0) to be middle of screen space
vOffset = { (float)(-ScreenWidth() / 2) / fScale, (float)(-ScreenHeight() / 2) / fScale };
return true;
}
bool OnUserUpdate(float fElapsedTime) override
{
// Get mouse location this frame
olc::vf2d vMouse = { (float)GetMouseX(), (float)GetMouseY() };
// Handle Pan & Zoom
if (GetMouse(2).bPressed)
{
vStartPan = vMouse;
}
if (GetMouse(2).bHeld)
{
vOffset -= (vMouse - vStartPan) / fScale;
vStartPan = vMouse;
}
olc::vf2d vMouseBeforeZoom;
ScreenToWorld((int)vMouse.x, (int)vMouse.y, vMouseBeforeZoom);
if (GetKey(olc::Key::Q).bHeld || GetMouseWheel() > 0)
{
fScale *= 1.1f;
}
if (GetKey(olc::Key::A).bHeld || GetMouseWheel() < 0)
{
fScale *= 0.9f;
}
olc::vf2d vMouseAfterZoom;
ScreenToWorld((int)vMouse.x, (int)vMouse.y, vMouseAfterZoom);
vOffset += (vMouseBeforeZoom - vMouseAfterZoom);
// Snap mouse cursor to nearest grid interval
vCursor.x = floorf((vMouseAfterZoom.x + 0.5f) * fGrid);
vCursor.y = floorf((vMouseAfterZoom.y + 0.5f) * fGrid);
if (GetKey(olc::Key::L).bPressed)
{
tempShape = new sLine();
// Place first node at location of keypress
selectedNode = tempShape->GetNextNode(vCursor);
// Get Second node
selectedNode = tempShape->GetNextNode(vCursor);
}
if (GetKey(olc::Key::B).bPressed)
{
tempShape = new sBox();
// Place first node at location of keypress
selectedNode = tempShape->GetNextNode(vCursor);
// Get Second node
selectedNode = tempShape->GetNextNode(vCursor);
}
if (GetKey(olc::Key::C).bPressed)
{
// Create new shape as a temporary
tempShape = new sCircle();
// Place first node at location of keypress
selectedNode = tempShape->GetNextNode(vCursor);
// Get Second node
selectedNode = tempShape->GetNextNode(vCursor);
}
if (GetKey(olc::Key::S).bPressed)
{
// Create new shape as a temporary
tempShape = new sCurve();
// Place first node at location of keypress
selectedNode = tempShape->GetNextNode(vCursor);
// Get Second node
selectedNode = tempShape->GetNextNode(vCursor);
}
// Search for any node that exists under the cursor, if one
// is found then select it
if (GetKey(olc::Key::M).bPressed)
{
selectedNode = nullptr;
for (auto &shape : listShapes)
{
selectedNode = shape->HitNode(vCursor);
if (selectedNode != nullptr)
break;
}
}
// If a node is selected, make it follow the mouse cursor
// by updating its position
if (selectedNode != nullptr)
{
selectedNode->pos = vCursor;
}
// As the user left clicks to place nodes, the shape can grow
// until it requires no more nodes, at which point it is completed
// and added to the list of completed shapes.
if (GetMouse(0).bReleased)
{
if (tempShape != nullptr)
{
selectedNode = tempShape->GetNextNode(vCursor);
if (selectedNode == nullptr)
{
tempShape->col = olc::WHITE;
listShapes.push_back(tempShape);
tempShape = nullptr; // Thanks @howlevergreen /Disord
}
}
else
{
selectedNode = nullptr;
}
}
// Clear Screen
Clear(olc::VERY_DARK_BLUE);
int sx, sy;
int ex, ey;
// Get visible world
olc::vf2d vWorldTopLeft, vWorldBottomRight;
ScreenToWorld(0, 0, vWorldTopLeft);
ScreenToWorld(ScreenWidth(), ScreenHeight(), vWorldBottomRight);
// Get values just beyond screen boundaries
vWorldTopLeft.x = floor(vWorldTopLeft.x);
vWorldTopLeft.y = floor(vWorldTopLeft.y);
vWorldBottomRight.x = ceil(vWorldBottomRight.x);
vWorldBottomRight.y = ceil(vWorldBottomRight.y);
// Draw Grid dots
for (float x = vWorldTopLeft.x; x < vWorldBottomRight.x; x += fGrid)
{
for (float y = vWorldTopLeft.y; y < vWorldBottomRight.y; y += fGrid)
{
WorldToScreen({ x, y }, sx, sy);
Draw(sx, sy, olc::BLUE);
}
}
// Draw World Axis
WorldToScreen({ 0,vWorldTopLeft.y }, sx, sy);
WorldToScreen({ 0,vWorldBottomRight.y }, ex, ey);
DrawLine(sx, sy, ex, ey, olc::GREY, 0xF0F0F0F0);
WorldToScreen({ vWorldTopLeft.x,0 }, sx, sy);
WorldToScreen({ vWorldBottomRight.x,0 }, ex, ey);
DrawLine(sx, sy, ex, ey, olc::GREY, 0xF0F0F0F0);
// Update shape translation coefficients
sShape::fWorldScale = fScale;
sShape::vWorldOffset = vOffset;
// Draw All Existing Shapes
for (auto &shape : listShapes)
{
shape->DrawYourself(this);
shape->DrawNodes(this);
}
// Draw shape currently being defined
if (tempShape != nullptr)
{
tempShape->DrawYourself(this);
tempShape->DrawNodes(this);
}
// Draw "Snapped" Cursor
WorldToScreen(vCursor, sx, sy);
DrawCircle(sx, sy, 3, olc::YELLOW);
// Draw Cursor Position
DrawString(10, 10, "X=" + std::to_string(vCursor.x) + ", Y=" + std::to_string(vCursor.x), olc::YELLOW, 2);
return true;
}
};
int main()
{
Polymorphism demo;
if (demo.Construct(800, 480, 1, 1, false))
demo.Start();
return 0;
}