Add practical polymorphism video + Dashed Lines + Mouse Wheel supportpull/113/head
parent
b3f124affd
commit
89d811fe33
@ -0,0 +1,536 @@ |
||||
/*
|
||||
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); |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
// 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; |
||||
} |
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in new issue