|
|
/*
|
|
|
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, <EFBFBD>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);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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.y), olc::YELLOW, 2);
|
|
|
return true;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
int main()
|
|
|
{
|
|
|
Polymorphism demo;
|
|
|
if (demo.Construct(1600, 960, 1, 1))
|
|
|
demo.Start();
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|