/* 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 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 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; }