Changeset View
Standalone View
source/gui/CChart.cpp
/* Copyright (C) 2017 Wildfire Games. | /* Copyright (C) 2018 Wildfire Games. | ||||
* This file is part of 0 A.D. | * This file is part of 0 A.D. | ||||
* | * | ||||
* 0 A.D. is free software: you can redistribute it and/or modify | * 0 A.D. is free software: you can redistribute it and/or modify | ||||
* it under the terms of the GNU General Public License as published by | * it under the terms of the GNU General Public License as published by | ||||
* the Free Software Foundation, either version 2 of the License, or | * the Free Software Foundation, either version 2 of the License, or | ||||
* (at your option) any later version. | * (at your option) any later version. | ||||
* | * | ||||
* 0 A.D. is distributed in the hope that it will be useful, | * 0 A.D. is distributed in the hope that it will be useful, | ||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
* GNU General Public License for more details. | * GNU General Public License for more details. | ||||
* | * | ||||
* You should have received a copy of the GNU General Public License | * You should have received a copy of the GNU General Public License | ||||
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. | * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>. | ||||
*/ | */ | ||||
#include "precompiled.h" | #include "precompiled.h" | ||||
#include "CChart.h" | #include "CChart.h" | ||||
#include "graphics/ShaderManager.h" | #include "graphics/ShaderManager.h" | ||||
#include "i18n/L10n.h" | |||||
#include "lib/ogl.h" | #include "lib/ogl.h" | ||||
#include "ps/CLogger.h" | #include "ps/CLogger.h" | ||||
#include "renderer/Renderer.h" | #include "renderer/Renderer.h" | ||||
#include "third_party/cppformat/format.h" | |||||
#include <cmath> | #include <cmath> | ||||
CChart::CChart() | CChart::CChart() | ||||
{ | { | ||||
AddSetting(GUIST_CColor, "axis_color"); | |||||
AddSetting(GUIST_float, "axis_width"); | |||||
AddSetting(GUIST_float, "buffer_zone"); | |||||
AddSetting(GUIST_CStrW, "font"); | |||||
AddSetting(GUIST_CStrW, "format_x"); | |||||
AddSetting(GUIST_CStrW, "format_y"); | |||||
AddSetting(GUIST_CGUIList, "series_color"); | AddSetting(GUIST_CGUIList, "series_color"); | ||||
AddSetting(GUIST_CGUISeries, "series"); | AddSetting(GUIST_CGUISeries, "series"); | ||||
AddSetting(GUIST_EAlign, "text_align"); | |||||
GUI<float>::GetSetting(this, "axis_width", m_AxisWidth); | |||||
GUI<CStrW>::GetSetting(this, "format_x", m_FormatX); | |||||
GUI<CStrW>::GetSetting(this, "format_y", m_FormatY); | |||||
} | } | ||||
CChart::~CChart() | CChart::~CChart() | ||||
{ | { | ||||
} | } | ||||
void CChart::HandleMessage(SGUIMessage& Message) | void CChart::HandleMessage(SGUIMessage& Message) | ||||
{ | { | ||||
// TODO: implement zoom | // TODO: implement zoom | ||||
switch (Message.type) | switch (Message.type) | ||||
{ | { | ||||
case GUIM_SETTINGS_UPDATED: | case GUIM_SETTINGS_UPDATED: | ||||
{ | { | ||||
GUI<float>::GetSetting(this, "axis_width", m_AxisWidth); | |||||
GUI<CStrW>::GetSetting(this, "format_x", m_FormatX); | |||||
GUI<CStrW>::GetSetting(this, "format_y", m_FormatY); | |||||
UpdateSeries(); | UpdateSeries(); | ||||
break; | break; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
void CChart::DrawLine(const CShaderProgramPtr& shader, const CColor& color, const std::vector<float>& vertices) const | void CChart::DrawLine(const CShaderProgramPtr& shader, const CColor& color, const std::vector<float>& vertices) const | ||||
{ | { | ||||
shader->Uniform(str_color, color); | shader->Uniform(str_color, color); | ||||
shader->VertexPointer(3, GL_FLOAT, 0, &vertices[0]); | shader->VertexPointer(3, GL_FLOAT, 0, &vertices[0]); | ||||
shader->AssertPointersBound(); | shader->AssertPointersBound(); | ||||
glEnable(GL_LINE_SMOOTH); | glEnable(GL_LINE_SMOOTH); | ||||
glLineWidth(1.1f); | glLineWidth(1.1f); | ||||
if (!g_Renderer.m_SkipSubmit) | if (!g_Renderer.m_SkipSubmit) | ||||
glDrawArrays(GL_LINE_STRIP, 0, vertices.size() / 3); | glDrawArrays(GL_LINE_STRIP, 0, vertices.size() / 3); | ||||
glLineWidth(1.0f); | glLineWidth(1.0f); | ||||
glDisable(GL_LINE_SMOOTH); | glDisable(GL_LINE_SMOOTH); | ||||
} | } | ||||
void CChart::DrawTriangleStrip(const CShaderProgramPtr& shader, const CColor& color, const std::vector<float>& vertices) const | |||||
{ | |||||
shader->Uniform(str_color, color); | |||||
shader->VertexPointer(3, GL_FLOAT, 0, &vertices[0]); | |||||
shader->AssertPointersBound(); | |||||
if (!g_Renderer.m_SkipSubmit) | |||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, vertices.size() / 3); | |||||
} | |||||
void CChart::DrawAxes(const CShaderProgramPtr& shader) const | |||||
{ | |||||
const float bz = GetBufferedZ(); | |||||
CRect rect = GetChartRect(); | |||||
std::vector<float> vertices; | |||||
vertices.reserve(30); | |||||
#define ADD(x, y) vertices.push_back(x); vertices.push_back(y); vertices.push_back(bz + 0.5f); | |||||
ADD(m_CachedActualSize.right, m_CachedActualSize.bottom); | |||||
ADD(rect.right + m_AxisWidth, rect.bottom); | |||||
ADD(m_CachedActualSize.left, m_CachedActualSize.bottom); | |||||
ADD(rect.left, rect.bottom); | |||||
ADD(m_CachedActualSize.left, m_CachedActualSize.top); | |||||
ADD(rect.left, rect.top - m_AxisWidth); | |||||
#undef ADD | |||||
CColor axis_color(0.5f, 0.5f, 0.5f, 1.f); | |||||
GUI<CColor>::GetSetting(this, "axis_color", axis_color); | |||||
DrawTriangleStrip(shader, axis_color, vertices); | |||||
} | |||||
s0600204: Why do the axes form a box, when just two sides are sufficient? | |||||
Not Done Inline ActionsTwo sides looks ugly. Mostly RPG/RTS uses boxes. Because UI has colored/imaged background. vladislavbelov: Two sides looks ugly. Mostly RPG/RTS uses boxes. Because UI has colored/imaged background. | |||||
Not Done Inline ActionsFour sides looks ugly, and it's not clear they are axes and not just sides of a box. Most graphs and charts use two axes (or three if there are two sets of overlaid data with different y scales). If gui designers want a box around the chart then they can add one via the use of <object type="image" sprite="BoxSpriteOfTheirChoiceAndOrDesign"/>. (And it should remain an option to have one or not.) And what does the gui background have to do with the number of chart axes? s0600204: Four sides looks ugly, and it's not clear they are axes and not just sides of a box.
Most… | |||||
Not Done Inline Actionsvladislavbelov: Sides != Axes. There are 2 axes, but 4 sides.
First results from Google:
AoE II:
{F210091}… | |||||
Not Done Inline ActionsAnd the point of this revision is to add axes to charts. Not sides. Of which there are, as you pointed out, two. And just to prove that I can use images too, a nice labeled image of x- and y-axes, courtesy of wikipedia: And the number of sides (or axes) does not make a chart any more or less visible. s0600204: And the point of this revision is to add //axes// to charts. Not sides. Of which there are, as… | |||||
Not Done Inline Actionsvladislavbelov: To add visible axes I add sides. Sides make charts much visible, because you see borders, you… | |||||
Not Done Inline ActionsGentlemen, it should be the XML file deciding how things should be styled, so just add an option to allow both? elexis: Gentlemen, it should be the XML file deciding how things should be styled, so just add an… | |||||
Not Done Inline Actions
Exactly. As I wrote months ago:
s0600204: >>! @elexis said,
> it should be the XML file deciding how things should be styled
> Ideally… | |||||
void CChart::Draw() | void CChart::Draw() | ||||
{ | { | ||||
PROFILE3("render chart"); | PROFILE3("render chart"); | ||||
if (!GetGUI()) | if (!GetGUI()) | ||||
return; | return; | ||||
if (m_Series.empty()) | if (m_Series.empty()) | ||||
Show All 11 Lines | void CChart::Draw() | ||||
// Setup the render state | // Setup the render state | ||||
CMatrix3D transform = GetDefaultGuiMatrix(); | CMatrix3D transform = GetDefaultGuiMatrix(); | ||||
CShaderDefines lineDefines; | CShaderDefines lineDefines; | ||||
CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid, g_Renderer.GetSystemShaderDefines(), lineDefines); | CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid, g_Renderer.GetSystemShaderDefines(), lineDefines); | ||||
tech->BeginPass(); | tech->BeginPass(); | ||||
CShaderProgramPtr shader = tech->GetShader(); | CShaderProgramPtr shader = tech->GetShader(); | ||||
shader->Uniform(str_transform, transform); | shader->Uniform(str_transform, transform); | ||||
CVector2D leftBottom, rightTop; | CVector2D scale(width / (m_RightTop.X - m_LeftBottom.X), height / (m_RightTop.Y - m_LeftBottom.Y)); | ||||
leftBottom = rightTop = m_Series[0].m_Points[0]; | |||||
for (const CChartData& data : m_Series) | |||||
for (const CVector2D& point : data.m_Points) | |||||
{ | |||||
if (fabs(point.X) != std::numeric_limits<float>::infinity() && point.X < leftBottom.X) | |||||
leftBottom.X = point.X; | |||||
if (fabs(point.Y) != std::numeric_limits<float>::infinity() && point.Y < leftBottom.Y) | |||||
leftBottom.Y = point.Y; | |||||
if (fabs(point.X) != std::numeric_limits<float>::infinity() && point.X > rightTop.X) | |||||
rightTop.X = point.X; | |||||
if (fabs(point.Y) != std::numeric_limits<float>::infinity() && point.Y > rightTop.Y) | |||||
rightTop.Y = point.Y; | |||||
} | |||||
if (rightTop.Y == leftBottom.Y) | |||||
rightTop.Y += 1; | |||||
if (rightTop.X == leftBottom.X) | |||||
rightTop.X += 1; | |||||
CVector2D scale(width / (rightTop.X - leftBottom.X), height / (rightTop.Y - leftBottom.Y)); | |||||
for (const CChartData& data : m_Series) | for (const CChartData& data : m_Series) | ||||
{ | { | ||||
if (data.m_Points.empty()) | if (data.m_Points.empty()) | ||||
continue; | continue; | ||||
std::vector<float> vertices; | std::vector<float> vertices; | ||||
for (const CVector2D& point : data.m_Points) | for (const CVector2D& point : data.m_Points) | ||||
{ | { | ||||
if (fabs(point.X) != std::numeric_limits<float>::infinity() && fabs(point.Y) != std::numeric_limits<float>::infinity()) | if (fabs(point.X) != std::numeric_limits<float>::infinity() && fabs(point.Y) != std::numeric_limits<float>::infinity()) | ||||
{ | { | ||||
vertices.push_back(rect.left + (point.X - leftBottom.X) * scale.X); | vertices.push_back(rect.left + (point.X - m_LeftBottom.X) * scale.X); | ||||
vertices.push_back(rect.bottom - (point.Y - leftBottom.Y) * scale.Y); | vertices.push_back(rect.bottom - (point.Y - m_LeftBottom.Y) * scale.Y); | ||||
vertices.push_back(bz + 0.5f); | vertices.push_back(bz + 0.5f); | ||||
} | } | ||||
else | else | ||||
{ | { | ||||
DrawLine(shader, data.m_Color, vertices); | DrawLine(shader, data.m_Color, vertices); | ||||
Not Done Inline Actionsrange based loop? elexis: range based loop? | |||||
Not Done Inline ActionsYou already suggested it and declined it by yourself :) We can't use range based loop here. vladislavbelov: You already suggested it and declined it by yourself :)
We can't use range based loop here. | |||||
Not Done Inline Actions(I almost had suggested it again) elexis: (I almost had suggested it again) | |||||
vertices.clear(); | vertices.clear(); | ||||
} | } | ||||
Not Done Inline Actionsaxis_font, axis_format_x/y? I suspect we might want to add other labels later. elexis: axis_font, axis_format_x/y? I suspect we might want to add other labels later. | |||||
Not Done Inline ActionsDo we really want to have other format_*? vladislavbelov: Do we really want to have other `format_*`? | |||||
Not Done Inline ActionsMaybe not formats, but fonts don't seem too unlikely. elexis: Maybe not formats, but fonts don't seem too unlikely.
But can keep as is as long as we have… | |||||
} | } | ||||
if (!vertices.empty()) | if (!vertices.empty()) | ||||
DrawLine(shader, data.m_Color, vertices); | DrawLine(shader, data.m_Color, vertices); | ||||
} | } | ||||
if (m_AxisWidth > 0) | |||||
DrawAxes(shader); | |||||
tech->EndPass(); | tech->EndPass(); | ||||
// Reset depth mask | // Reset depth mask | ||||
glDepthMask(1); | glDepthMask(1); | ||||
for (size_t i = 0; i < m_TextPositions.size(); ++i) | |||||
DrawText(i, CColor(1.f, 1.f, 1.f, 1.f), m_TextPositions[i], bz + 0.5f); | |||||
Not Done Inline Actionscould use a range based loop elexis: could use a range based loop | |||||
Not Done Inline Actionssorry can't read :-X elexis: sorry can't read :-X | |||||
} | } | ||||
CRect CChart::GetChartRect() const | CRect CChart::GetChartRect() const | ||||
{ | { | ||||
return m_CachedActualSize; | return CRect( | ||||
m_CachedActualSize.TopLeft() + CPos(m_AxisWidth, m_AxisWidth), | |||||
m_CachedActualSize.BottomRight() - CPos(m_AxisWidth, m_AxisWidth) | |||||
); | |||||
} | } | ||||
void CChart::UpdateSeries() | void CChart::UpdateSeries() | ||||
{ | { | ||||
CGUISeries* pSeries; | CGUISeries* pSeries; | ||||
GUI<CGUISeries>::GetSettingPointer(this, "series", pSeries); | GUI<CGUISeries>::GetSettingPointer(this, "series", pSeries); | ||||
CGUIList* pSeriesColor; | CGUIList* pSeriesColor; | ||||
GUI<CGUIList>::GetSettingPointer(this, "series_color", pSeriesColor); | GUI<CGUIList>::GetSettingPointer(this, "series_color", pSeriesColor); | ||||
m_Series.clear(); | m_Series.clear(); | ||||
m_Series.resize(pSeries->m_Series.size()); | m_Series.resize(pSeries->m_Series.size()); | ||||
for (size_t i = 0; i < pSeries->m_Series.size(); ++i) | for (size_t i = 0; i < pSeries->m_Series.size(); ++i) | ||||
{ | { | ||||
CChartData& data = m_Series[i]; | CChartData& data = m_Series[i]; | ||||
if (i < pSeriesColor->m_Items.size() && !GUI<int>::ParseColor(pSeriesColor->m_Items[i].GetOriginalString(), data.m_Color, 0)) | if (i < pSeriesColor->m_Items.size() && !GUI<int>::ParseColor(pSeriesColor->m_Items[i].GetOriginalString(), data.m_Color, 0)) | ||||
LOGWARNING("GUI: Error parsing 'series_color' (\"%s\")", utf8_from_wstring(pSeriesColor->m_Items[i].GetOriginalString())); | LOGWARNING("GUI: Error parsing 'series_color' (\"%s\")", utf8_from_wstring(pSeriesColor->m_Items[i].GetOriginalString())); | ||||
data.m_Points = pSeries->m_Series[i]; | data.m_Points = pSeries->m_Series[i]; | ||||
} | } | ||||
UpdateBounds(); | |||||
SetupText(); | |||||
} | |||||
void CChart::SetupText() | |||||
{ | |||||
if (!GetGUI()) | |||||
return; | |||||
for (SGUIText* t : m_GeneratedTexts) | |||||
delete t; | |||||
m_GeneratedTexts.clear(); | |||||
m_TextPositions.clear(); | |||||
if (m_Series.empty()) | |||||
return; | |||||
CStrW font; | |||||
if (GUI<CStrW>::GetSetting(this, "font", font) != PSRETURN_OK || font.empty()) | |||||
font = L"default"; | |||||
Not Done Inline Actions// unneeded comment // TODO Vladslav: Don't duplicate TODOs from 2004 needlessly elexis: ```
// unneeded comment
// TODO Vladslav: Don't duplicate TODOs from 2004 needlessly
``` | |||||
Not Done Inline ActionsWhy do you think it's unneeded? vladislavbelov: Why do you think it's unneeded? | |||||
Not Done Inline Actions
IMO The first line is unneeded because the code is equally easy to read. IMO The second line is needless because we have that comment at least once already. Someone caring about this has to search the code for all occurrences of the bugged code (not for all occurrences of the comment). elexis: > Why?
IMO The first line is unneeded because the code is equally easy to read.
IMO The… | |||||
Not Done Inline ActionsExactly by this reason I leave it as is, because it's easier to find it here. It will look strange if we would have this comment in many places, but in the one - not. vladislavbelov: Exactly by this reason I leave it as is, because it's easier to find it here. It will look… | |||||
float buffer_zone = 0.f; | |||||
GUI<float>::GetSetting(this, "buffer_zone", buffer_zone); | |||||
// Add Y-axis | |||||
GUI<CStrW>::GetSetting(this, "format_y", m_FormatY); | |||||
const float height = GetChartRect().GetHeight(); | |||||
// TODO: split values depend on the format; | |||||
if (m_EqualY) | |||||
{ | |||||
// We don't need to generate many items for equal values | |||||
AddFormattedValue(m_FormatY, m_RightTop.Y, font, buffer_zone); | |||||
m_TextPositions.emplace_back(GetChartRect().TopLeft()); | |||||
} | |||||
else | |||||
for (int i = 0; i < 3; ++i) | |||||
{ | |||||
AddFormattedValue(m_FormatY, m_RightTop.Y - (m_RightTop.Y - m_LeftBottom.Y) / 3.f * i, font, buffer_zone); | |||||
m_TextPositions.emplace_back(GetChartRect().TopLeft() + CPos(0.f, height / 3.f * i)); | |||||
} | |||||
// Add X-axis | |||||
GUI<CStrW>::GetSetting(this, "format_x", m_FormatX); | |||||
const float width = GetChartRect().GetWidth(); | |||||
if (m_EqualX) | |||||
{ | |||||
CSize text_size = AddFormattedValue(m_FormatX, m_RightTop.X, font, buffer_zone); | |||||
Not Done Inline ActionsDoes it have to be limited to 64? There are ways to convert a float to a string and a string to a char array without that limitation elexis: Does it have to be limited to 64? There are ways to convert a float to a string and a string to… | |||||
Not Done Inline ActionsWe use format string, not simple conversion (format string could contain a text). So I didn't find the better way yet. vladislavbelov: We use format string, not simple conversion (format string could contain a text). So I didn't… | |||||
Not Done Inline ActionsBut why do we use format strings? What if someone does strange things (there is no limit on format anywhere) with the passed format string (which does come from JS/XML)? Eg using %n, not that having access to JS and XML wouldn't already be an issue, but why add code that just begs to be abused? leper: But why do we use format strings?
What if someone does strange things (there is no limit on… | |||||
Not Done Inline ActionsThat could become a buffer overflow vulnerability if we have input from untrusted sources, i.e. if people could collaboratively style the format of charts and have format being sent across the network. If we don't trust gui/summary/, but source/, then we should exclusively add support for few well defined formats, like printing a number with a user defined amount of decimals, percent numbers (to get the % character) and maybe ingame-time for events? elexis: That could become a buffer overflow vulnerability if we have input from untrusted sources, i.e. | |||||
Not Done Inline ActionsIt currently is one, however it also is a format string issue (which is why I mentioned %n, there's quite some literature about that in case you are bored). I wouldn't say we don't trust the gui, but I'd rather not add code that relies on us always trusting it when there are alternatives where we don't have to. Always assume that you will get input you don't expect, so handle things to cope with those (even if the result is something that isn't useful, at least it shouldn't be harmful). leper: It currently is one, however it also is a format string issue (which is why I mentioned `%n`… | |||||
Not Done Inline ActionsJust provide a helper function for each hardcoded format string. We really need axis labels. elexis: Just provide a helper function for each hardcoded format string. We really need axis labels. | |||||
m_TextPositions.emplace_back(GetChartRect().BottomRight() - text_size); | |||||
} | |||||
else | |||||
for (int i = 0; i < 3; ++i) | |||||
{ | |||||
CSize text_size = AddFormattedValue(m_FormatX, m_RightTop.X - (m_RightTop.X - m_LeftBottom.X) / 3 * i, font, buffer_zone); | |||||
m_TextPositions.emplace_back(GetChartRect().BottomRight() - text_size - CPos(width / 3 * i, 0.f)); | |||||
} | |||||
} | |||||
Not Done Inline Actions++? elexis: ++? | |||||
Not Done Inline ActionsIt's not really good looking for decimal. I think, += 1.f is better. vladislavbelov: It's not really good looking for decimal. I think, `+= 1.f` is better. | |||||
CSize CChart::AddFormattedValue(const CStrW& format, const float value, const CStrW& font, const float buffer_zone) | |||||
{ | |||||
// TODO: we need to catch cases with equal formatted values. | |||||
CGUIString gui_str; | |||||
if (format == L"DECIMAL2") | |||||
{ | |||||
wchar_t buffer[64]; | |||||
swprintf(buffer, 64, L"%.2f", value); | |||||
gui_str.SetValue(buffer); | |||||
} | |||||
else if (format == L"INTEGER") | |||||
{ | |||||
wchar_t buffer[64]; | |||||
swprintf(buffer, 64, L"%d", static_cast<int>(value)); | |||||
gui_str.SetValue(buffer); | |||||
} | |||||
else if (format == L"DURATION_SHORT") | |||||
{ | |||||
const int seconds = value; | |||||
wchar_t buffer[64]; | |||||
swprintf(buffer, 64, L"%d:%02d", seconds / 60, seconds % 60); | |||||
gui_str.SetValue(buffer); | |||||
} | |||||
else | |||||
{ | |||||
LOGERROR("Unsupported chart format: " + format.EscapeToPrintableASCII()); | |||||
return CSize(); | |||||
} | |||||
SGUIText* text = new SGUIText(); | |||||
*text = GetGUI()->GenerateText(gui_str, font, 0, buffer_zone, this); | |||||
AddText(text); | |||||
return text->m_Size; | |||||
} | |||||
void CChart::UpdateBounds() | |||||
{ | |||||
if (m_Series.empty() || m_Series[0].m_Points.empty()) | |||||
{ | |||||
m_LeftBottom = m_RightTop = CVector2D(0.f, 0.f); | |||||
return; | |||||
} | |||||
m_LeftBottom = m_RightTop = m_Series[0].m_Points[0]; | |||||
for (const CChartData& data : m_Series) | |||||
for (const CVector2D& point : data.m_Points) | |||||
{ | |||||
if (fabs(point.X) != std::numeric_limits<float>::infinity() && point.X < m_LeftBottom.X) | |||||
m_LeftBottom.X = point.X; | |||||
if (fabs(point.Y) != std::numeric_limits<float>::infinity() && point.Y < m_LeftBottom.Y) | |||||
m_LeftBottom.Y = point.Y; | |||||
if (fabs(point.X) != std::numeric_limits<float>::infinity() && point.X > m_RightTop.X) | |||||
m_RightTop.X = point.X; | |||||
if (fabs(point.Y) != std::numeric_limits<float>::infinity() && point.Y > m_RightTop.Y) | |||||
m_RightTop.Y = point.Y; | |||||
} | |||||
m_EqualY = m_RightTop.Y == m_LeftBottom.Y; | |||||
if (m_EqualY) | |||||
m_RightTop.Y += 1; | |||||
m_EqualX = m_RightTop.X == m_LeftBottom.X; | |||||
if (m_EqualX) | |||||
m_RightTop.X += 1; | |||||
} | } |
Why do the axes form a box, when just two sides are sufficient?