/** * @file Polygon.cpp * * @author Charles Owen */ #include "pch.h" #include <sstream> #include <wx/hyperlink.h> #include "Polygon.h" using namespace cse335; /** * Constructor */ Polygon::Polygon() : mBrush(*wxBLACK) { } /** * Destructor */ Polygon::~Polygon() { } void Polygon::AddPoint(int x, int y) { if(mHasDrawn) { // Our polygon MUST have at least three points Assert(false, L"You cannot add points to the polygon after it has been drawn.", L"https://facweb.cse.msu.edu/cbowen/cse335/polygon/o/"); return; } mPoints.push_back(wxPoint(x, y)); } /** * Create a rectangle. * * If the height parameter is not supplied and an image * has been set, the height will be computed so as to * keep the original image aspect ratio correct. * * @param x Left side X * @param y Bottom left Y * @param width Width of the rectangle * @param height Height of the rectangle */ void Polygon::Rectangle(int x, int y, int width, int height) { if(width <= 0) { // Optional automatic width determination from image if(!Assert(mImage != nullptr, L"You must select an image before calling Rectangle with no specified width.")) { return; } width = mImage->GetWidth(); } if(height <= 0) { // Optional automatic height determination from image if(!Assert(mImage != nullptr, L"You must select an image before calling Rectangle with no specified height.")) { return; } height = (int)(width * mImage->GetHeight() / mImage->GetWidth()); } AddPoint(x, y); AddPoint(x, y - height); AddPoint(x + width, y - height); AddPoint(x + width, y); } /** * Create a rectangle where 0,0 is the bottom center of the rectangle. * * Must set an image first if not providing a width and height. * * @param width Width of the rectangle in pixels. If not supplied, use the image width. * @param height Height of the rectangle in pixels. If not supplied, select a height such * that the aspect ratio matches that of the image. */ void Polygon::BottomCenteredRectangle(int width, int height) { if(width == 0) { if(!Assert(mImage != nullptr, L"You must select an image before calling BottomCenteredRectangle with no width.")) { return; } width = GetImageWidth(); } else if(height == 0) { if(!Assert(mImage != nullptr, L"You must select an image before calling BottomCenteredRectangle with no height.")) { return; } height = width * GetImageHeight() / GetImageWidth(); } Rectangle(-width/2, 0, width, height); } /** * Create a centered square at location 0,0 * @param size Width and height of the square */ void Polygon::CenteredSquare(int size) { if(size == 0) { if(!Assert(mImage != nullptr, L"You must select an image before calling BottomCenteredRectangle.")) { return; } size = mImage->GetWidth(); } Rectangle(-size / 2, size / 2, size, size); } /** * Create a circle centered on (0,0) * @param radius Circle radius * @param steps Number of steps in circle (0=default) */ void Polygon::Circle(double radius, int steps) { for (int i = 0; i < steps; i++) { double angle = double(i) / double(steps) * M_PI * 2; AddPoint(radius * cos(angle), radius * sin(angle)); } } /** * Set the color of the polygon. If we set a color, images are not used. * @param color A Gdiplus Color object. */ void Polygon::SetColor(wxColour color) { mBrush.SetColour(color); mMode = Mode::Color; } /** * Set an image we will use as a texture for the polygon * @param filename Image filename */ void Polygon::SetImage(std::wstring filename) { // Prevent error popup from wxWidgets wxLogNull logNo; mImage = std::make_unique<wxImage>(); if(mImage->LoadFile(filename, wxBITMAP_TYPE_ANY)) { mMode = Mode::Image; } else { std::wstringstream str; str << L"Unable to load '" << filename << "'" << std::endl; wxMessageBox(str.str(), L"Polygon Image File Load Failure!"); mImage = nullptr; } } /** * Draw the polygon * @param graphics Graphics object to draw on * @param x X location to draw in pixels * @param y Y location to draw in pixels */ void Polygon::DrawPolygon(std::shared_ptr<wxGraphicsContext> graphics, int x, int y) { if(mPoints.size() < 3) { // Our polygon MUST have at least three points Assert(false, L"You must specify a shape when using Polygon. At least three points must be provided.", L"https://facweb.cse.msu.edu/cbowen/cse335/polygon/c/"); return; } mHasDrawn = true; #ifndef WIN32 if(mOpacity < 1) { // Layer opacity does not work on Windows systems. graphics->BeginLayer(mOpacity); } #endif switch (mMode) { case Mode::Color: DrawColorPolygon(graphics, x, y); break; case Mode::Image: DrawImagePolygon(graphics, x, y); break; default: // If this assertion fails, the no color or image was // indicated for this polygon. Be sure to call Assert(false, L"You must specify either a color or an image when using Polygon", L"https://facweb.cse.msu.edu/cbowen/cse335/polygon/c/"); break; } #ifndef WIN32 if(mOpacity < 1) { graphics->EndLayer(); } #endif } /** * Draw the polygon as a solid color-filled polygon * @param graphics Graphics object to draw on * @param x X location to draw in pixels * @param y Y location to draw in pixels */ void Polygon::DrawColorPolygon(std::shared_ptr<wxGraphicsContext> graphics, int x, int y) { if(mPath.IsNull()) { // Create the graphics path mPath = graphics->CreatePath(); mPath.MoveToPoint(mPoints[0].x, mPoints[0].y); for(size_t i=1; i<mPoints.size(); i++) { mPath.AddLineToPoint(mPoints[i].x, mPoints[i].y); } mPath.CloseSubpath(); } graphics->PushState(); graphics->Translate(x, y); graphics->Rotate(mRotation * M_PI * 2); graphics->SetBrush(mBrush); graphics->FillPath(mPath); graphics->PopState(); } /** * Draw the polygon as a texture mapped image. * * This is accomplished by drawing the bitmap image clipped * by the supplied polygon points. * * @param graphics Graphics object to draw on * @param x X location to draw in pixels * @param y Y location to draw in pixels */ void Polygon::DrawImagePolygon(std::shared_ptr<wxGraphicsContext> graphics, int x, int y) { if(mBitmapDirty || mGraphicsBitmap.IsNull()) { #ifdef WIN32 // Implementation of opacity for Windows systems. // Windows does not support transparency layers. if(mOpacity < 1) { // Ensure the image has an alpha map if (!mImage->HasAlpha()) { mImage->InitAlpha(); } wxImage img = mImage->Copy(); unsigned char *alpha = img.GetAlpha(); for(int i=0; i<img.GetWidth()*img.GetHeight(); i++) { alpha[i] = int(alpha[i] * mOpacity); } mGraphicsBitmap = graphics->CreateBitmapFromImage(img); } else { mGraphicsBitmap = graphics->CreateBitmapFromImage(*mImage); } #else mGraphicsBitmap = graphics->CreateBitmapFromImage(*mImage); #endif // // Determine the top left and the size of the // region covered by our polygon // mImageClipRegionTopLeft = mPoints[0]; auto imageClipRegionBottomRight = mPoints[0]; for(auto point : mPoints) { if(point.x < mImageClipRegionTopLeft.x) { mImageClipRegionTopLeft.x = point.x; } if(point.y < mImageClipRegionTopLeft.y) { mImageClipRegionTopLeft.y = point.y; } if(point.x > imageClipRegionBottomRight.x) { imageClipRegionBottomRight.x = point.x; } if(point.y > imageClipRegionBottomRight.y) { imageClipRegionBottomRight.y = point.y; } } mImageClipRegionSize = imageClipRegionBottomRight - mImageClipRegionTopLeft; mImageClipRegion.Clear(); std::vector<wxPoint> points; for(auto point : mPoints) { points.push_back(wxPoint(point.x - mImageClipRegionTopLeft.x, point.y - mImageClipRegionTopLeft.y)); } mImageClipRegion = wxRegion(points.size(), &points[0]); mBitmapDirty = false; } graphics->PushState(); graphics->Translate(x, y); graphics->Rotate(mRotation * M_PI * 2); graphics->Translate(mImageClipRegionTopLeft.x, mImageClipRegionTopLeft.y); graphics->Clip(mImageClipRegion); graphics->DrawBitmap(mGraphicsBitmap, 0, 0, mImageClipRegionSize.x, mImageClipRegionSize.y); graphics->ResetClip(); graphics->PopState(); } /** * Convenience function to draw a crosshair. * @param graphics Graphics object to draw on * @param x X location for crosshair center * @param y Y location for crosshair center * @param size Size (width and height) of the crosshair in pixels (optional, default= * @param color Crosshair color (optional, default=red) */ void Polygon::DrawCrosshair(std::shared_ptr<wxGraphicsContext> graphics, int x, int y, int size, wxColor color) { wxPen pen(color); graphics->SetPen(pen); graphics->StrokeLine(x-size/2, y, x+size/2, y); graphics->StrokeLine(x, y-size/2, x, y+size/2); } /** * Get the width of any set image for this polygon. This may not be * the width we actually draw, but is the size of the polygon itself. * @return Width in pixels */ int Polygon::GetImageWidth() { if(!Assert(mImage != nullptr, L"You must specify an image before you can call GetImageWidth()")) { return 0; } return mImage->GetWidth(); } /** * Get the height of any set image for this polygon. This may not be * the height we actually draw, but is the size of the polygon itself. * @return Height in pixels */ int Polygon::GetImageHeight() { if(!Assert(mImage != nullptr, L"You must specify an image before you can call GetImageHeight()")) { return 0; } return mImage->GetHeight(); } /** * Assertion for Polygon that provides pop-up help. * @param condition Condition that is expected to the true. * @param msg Message that is provide if the condition is not true * @param url Optional URL to display with the error * @return Assertion condition result, true if condition is true */ bool Polygon::Assert(bool condition, wxString msg, const wxString &url) { if(condition) { return true; } // Set a breakpoint on this line to determine where // in your code the error comes from. if(mDelayedMessage == nullptr) { mDelayedMessage = std::make_shared<DelayedMessage>(); } mDelayedMessage->Fire(msg, url); return false; } /** * Set the opacity of the polygon rendering. * * Currently only works for images. * * @param opacity Opacity from 0 to 1 */ void Polygon::SetOpacity(double opacity) { if(opacity != mOpacity) { if(!Assert(opacity >= 0 && opacity <= 1, L"Values passed to Polygon::SetOpacity must be in the range 0 to 1.")) { return; } // We have an opacity change mOpacity = opacity; #ifdef WIN32 mBitmapDirty = true; #endif } } /** * Get the average luminance of a block of pixels in any supplied image. * @param x Top left X in pixels * @param y Top left Y in pixels * @param wid Width of the block to average * @param hit Height of the block to average * @return Luminance in the range 0-1, where 0 is black. */ double Polygon::AverageLuminance(int x, int y, int wid, int hit) { assert(mMode == Mode::Image); double sum = 0; int cnt = 0; for (int i = x; i < x + wid; i++) { if (i < 0 || i >= GetImageWidth()) { continue; } for (int j = y; j < y + hit; j++) { if (j < 0 || j >= GetImageHeight()) { continue; } // Gdiplus::Color color; // mTexture->GetPixel(i, j, &color); double red = mImage->GetRed(i, j); double grn = mImage->GetGreen(i, j); double blu = mImage->GetBlue(i, j); sum += red + grn + blu; cnt += 3; } } if (cnt == 0) { return 0; } return (sum / cnt) / 255.0; } //editor-fold desc="Code to support the deferred assertion message box" defaultstate="collapsed"> /** * Fire a message display after a delay. This is done since it is not * possible to bring up a dialog box in a Draw function, which is * where most errors occur. * @param msg Message to display * @param url Optional URL for help */ void Polygon::DelayedMessage::Fire(const wxString& msg, const wxString& url) { if(mFired) { return; } mMessage = msg; mURL = url; StartOnce(10); mFired = true; } /** * Handle the timer event so we can display the dialog box. */ void Polygon::DelayedMessage::Notify() { wxDialog dialog(wxTheApp->GetTopWindow(), wxID_ANY, L"Polygon Class Usage Error"); dialog.SetSizeHints( wxDefaultSize, wxDefaultSize ); auto sizer = new wxBoxSizer( wxVERTICAL ); auto m_staticText1 = new wxStaticText( &dialog, wxID_ANY, mMessage, wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER_HORIZONTAL ); m_staticText1->Wrap( 300 ); sizer->Add( m_staticText1, 0, wxALL|wxEXPAND, 15 ); if(!mURL.IsEmpty()) { auto m_staticText2 = new wxHyperlinkCtrl( &dialog, wxID_ANY, mURL, mURL); sizer->Add( m_staticText2, 0, wxALL, 5 ); } auto m_button1 = new wxButton( &dialog, wxID_OK, wxT("Ok"), wxDefaultPosition, wxDefaultSize, 0 ); sizer->Add( m_button1, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); dialog.SetSizer( sizer ); dialog.Layout(); sizer->Fit( &dialog ); dialog.Centre(wxBOTH); dialog.ShowModal(); } ///editor-fold>