spacebox/src/Box.cpp

616 lines
17 KiB
C++

/* /\ +--------------------------------------------------------------+
____/ \____ /| - zlib/MIT/Unlicenced game framework licensed to freely use, |
\ / / | copy, and modify without restriction |
+--\ ^__^ /--+ | |
| ~/ \~ | | - originally created at [http://nugget.fun] |
| ~~~~~~~~~~~~ | +--------------------------------------------------------------+
| SPACE ~~~~~ | /
| ~~~~~~~ BOX |/
+-------------*/
#include "Box.hpp"
/* Construct a Box by giving a corner coordinate (top left if Y-axis is not inverted, bottom left otherwise) and
* size (as width, height). The invert_y argument indicates that the Y-coordinate increases from the bottom of
* the screen to the top rather than the default case where it increases from the top to the bottom. */
Box::Box(const glm::vec2& corner, const glm::vec2& size, bool invert_y)
{
x = corner.x;
y = corner.y;
w = size.x;
h = size.y;
this->invert_y(invert_y);
}
/* Construct a Box from float arguments: x, y, width, height. If invert_y is set, the Y-axis increases from bottom to
* top instead of top to bottom. */
Box::Box(float x, float y, float width, float height, bool invert_y) : Box({x, y}, {width, height}, invert_y) {}
/* Construct a Box by passing an SDL_Rect struct, which is of the form {x, y, w, h} and limited to int arguments. */
Box::Box(const SDL_Rect& rect, bool invert_y) : Box({rect.x, rect.y}, {rect.w, rect.h}, invert_y) {}
/* Set inverted Y mode to true, meaning the Y-coordinate increases from the bottom to the top, or false, meaning the
* Y-coordinate increases from the top to the bottom. */
void Box::invert_y(bool invert)
{
invert_y_state = invert;
}
/* Returns true if Y-axis was set to inverted, meaning the Y-coordinate increases from the bottom to the top, or false,
* meaning Y-coordinate increases from the top to the bottom. */
bool Box::inverted_y() const
{
return invert_y_state;
}
/* Return the width */
float Box::width() const
{
return w;
}
/* Set the width. Negative values will be clamped to zero. */
void Box::width(float width)
{
w = std::max(width, 0.0f);
}
/* Return the height */
float Box::height() const
{
return h;
}
/* Set the height. Negative values will be clamped to zero. */
void Box::height(float height)
{
h = std::max(height, 0.0f);
}
/* Return the size as a vector {width, height} */
glm::vec2 Box::size() const
{
return {width(), height()};
}
/* Set the size. Negative values will be clamped to zero. */
void Box::size(const glm::vec2& size, bool preserve_center)
{
glm::vec2 center = this->center();
width(size.x);
height(size.y);
if (preserve_center)
{
this->center(center);
}
}
float Box::aspect(bool larger_ratio) const
{
if (!larger_ratio || width() > height())
{
return width() / height();
}
else
{
return height() / width();
}
}
/* Return the area of the box (width * height) */
float Box::area() const
{
return width() * height();
}
/* Return the y coordinate representing the top of the box */
float Box::top() const
{
/* if Y-coordinate increases from bottom to top, use the bottom to determine this value */
if (inverted_y())
{
return bottom() + height();
}
/* otherwise use the corner Y-coordinate */
else
{
return y;
}
}
/* Return the x coordinate representing the right side of the box */
float Box::right() const
{
return left() + width();
}
/* Return the y coordinate representing the bottom of the box */
float Box::bottom() const
{
/* if Y-coordinate increases from bottom to top, use the stored Y-coordinate */
if (inverted_y())
{
return y;
}
/* otherwise use the top to determine the bottom value */
else
{
return top() + height();
}
}
/* Return the x coordinate representing the left side of the box */
float Box::left() const
{
return x;
}
/* Return the x component of the center coordinates */
float Box::cx() const
{
return left() + width() / 2.0f;
}
/* Return the y component of the center coordinates */
float Box::cy() const
{
/* if Y-coordinate increases from bottom to top, add from the bottom */
if (inverted_y())
{
return bottom() + height() / 2.0f;
}
/* otherwise add from the top */
else
{
return top() + height() / 2.0f;
}
}
/* Set the y coordinate of the top of the box. If drag is set, the other sides of the box will remain in the
* same position, and the top will be moved, altering the size of the box. Otherwise, the box will remain the
* same size, and the bottom location will change, moving the box but preserving the size. */
void Box::top(float top, bool drag)
{
float delta = top - this->top();
if (!drag)
{
move({0, delta});
}
else
{
drag_top(delta);
}
}
/* Drag the top delta amount, leaving the bottom in the same position and altering the size of the box. The top
* can be dragged past the bottom, in which case the current bottom will become the new top value. */
void Box::drag_top(float delta)
{
float new_location = top() + delta;
height(std::abs(bottom() - new_location));
if ((inverted_y() && new_location < bottom()) || (!inverted_y() && new_location > bottom()))
{
bottom(new_location);
}
else
{
top(new_location);
}
}
/* Set the x coordinate of the right side of the box. If drag is set, the other sides of the box will remain in the
* same position, and the right will be moved, altering the size of the box. Otherwise, the box will remain the
* same size, and the left location will change, moving the box but preserving the size. */
void Box::right(float right, bool drag)
{
float delta = right - this->right();
if (!drag)
{
move({delta, 0});
}
else
{
drag_right(delta);
}
}
/* Drag the right side delta amount, leaving the left in the same position and altering the size of the box. The right
* side can be dragged past the left, in which case the current left will become the new right value. */
void Box::drag_right(float delta)
{
float new_location = right() + delta;
width(std::abs(left() - new_location));
if (new_location < left())
{
left(new_location);
}
}
/* Set the y coordinate of the bottom of the box. If drag is set, the other sides of the box will remain in the
* same position, and the bottom will be moved, altering the size of the box. Otherwise, the box will remain the
* same size, and the top location will change, moving the box but preserving the size. */
void Box::bottom(float bottom, bool drag)
{
float delta = bottom - this->bottom();
if (!drag)
{
move({0, delta});
}
else
{
drag_bottom(delta);
}
}
/* Drag the bottom delta amount, leaving the top in the same position and altering the size of the box. The bottom
* can be dragged past the top, in which case the current top will become the new bottom value. */
void Box::drag_bottom(float delta)
{
float new_location = bottom() + delta;
height(std::abs(top() - new_location));
if ((inverted_y() && new_location > top()) || (!inverted_y() && new_location < bottom()))
{
top(new_location);
}
else
{
bottom(new_location);
}
}
/* Set the x coordinate of the left side of the box. If drag is set, the other sides of the box will remain in the
* same position, and the left will be moved, altering the size of the box. Otherwise, the box will remain the
* same size, and the right location will change, moving the box but preserving the size. */
void Box::left(float left, bool drag)
{
float delta = left - this->left();
if (!drag)
{
move({delta, 0});
}
else
{
drag_left(delta);
}
}
/* Drag the left side delta amount, leaving the right side in the same position and altering the size of the box.
* The left side can be dragged past the right, in which case the current right will become the new left value. */
void Box::drag_left(float delta)
{
float new_location = left() + delta;
if (new_location > right())
{
left(right());
width(new_location - left());
}
else
{
width(right() - new_location);
left(new_location);
}
}
/* Set the center x component */
void Box::cx(float cx)
{
move({cx - this->cx(), 0.0f});
}
/* Set the center y component */
void Box::cy(float cy)
{
move({0.0f, cy - this->cy()});
}
/* Return the coordinates of the top left corner */
glm::vec2 Box::nw() const
{
return {left(), top()};
}
/* Return the coordinates of the center of the top edge */
glm::vec2 Box::north() const
{
return {left() + width() / 2.0f, top()};
}
/* Return the coordinates of the top right corner */
glm::vec2 Box::ne() const
{
return {right(), top()};
}
/* Return the coordinates of the center of the right edge */
glm::vec2 Box::east() const
{
return {right(), top() + height() / 2.0f};
}
/* Return the coordinates of the bottom right corner */
glm::vec2 Box::se() const
{
return {right(), bottom()};
}
/* Return the coordinates of the center of the bottom edge */
glm::vec2 Box::south() const
{
return {left() + width() / 2, bottom()};
}
/* Return the coordinates of the bottom left edge */
glm::vec2 Box::sw() const
{
return {left(), bottom()};
}
/* Return the coordinates of the center of the left edge */
glm::vec2 Box::west() const
{
return {left(), top() + height() / 2.0f};
}
/* Return the center coordinates */
glm::vec2 Box::center() const
{
return {cx(), cy()};
}
/* Move the box by specifying the top left corner coordinates */
void Box::nw(const glm::vec2& nw)
{
move(nw - this->nw());
}
/* Move the box by specifying the center of the top edge */
void Box::north(const glm::vec2& north)
{
move(north - this->north());
}
/* Move the box by specifying the top right corner coordinates */
void Box::ne(const glm::vec2& ne)
{
move(ne - this->ne());
}
/* Move the box by specifying the center of the right edge */
void Box::east(const glm::vec2& east)
{
move(east - this->east());
}
/* Move the box by specifying the bottom right corner coordinates */
void Box::se(const glm::vec2& se)
{
move(se - this->se());
}
/* Move the box by specifying the center of the bottom edge */
void Box::south(const glm::vec2& south)
{
move(south - this->south());
}
/* Move the box by specifying the bottom left corner coordinates */
void Box::sw(const glm::vec2& sw)
{
move(sw - this->sw());
}
/* Move the box by specifying the center of the left edge */
void Box::west(const glm::vec2& west)
{
move(west - this->west());
}
/* Move the box by specifying the center coordinates */
void Box::center(const glm::vec2& center)
{
move(center - this->center());
}
/* Called when a Box instance is converted into an SDL_Rect. Allows passing Box to SDL functions that
* expect an SDL_Rect instead of an SDL_FRect */
Box::operator SDL_Rect() const
{
return {static_cast<int>(left()), static_cast<int>(top()), static_cast<int>(width()), static_cast<int>(height())};
}
/* Zero out the values of the box coordinates and size */
void Box::clear()
{
x = 0;
y = 0;
w = 0;
h = 0;
}
/* Scale the box by multiplying size by {delta.x, delta.y}. If preserve center is set, the box will be scaled
* around the center, otherwise the top left corner will remain in place. */
void Box::scale(glm::vec2 delta, bool preserve_center)
{
glm::vec2 center = this->center();
size(this->size() * delta);
if (preserve_center)
{
this->center(center);
}
}
/* Scale the box by multiplying both width and height by delta. If preserve center is set, the box will be scaled
* around the center, otherwise the top left corner will remain in place. */
void Box::scale(float delta, bool preserve_center)
{
scale({delta, delta}, preserve_center);
}
/* Expand (or shrink by passing negative values) the box size by adding delta to the current size. If preserve center
* is set, the box will expand around the center, otherwise the top left corner will remain in place. */
void Box::expand(glm::vec2 delta, bool preserve_center)
{
glm::vec2 center = this->center();
size(size() + delta);
if (preserve_center)
{
this->center(center);
}
}
/* Expand (or shrink by passing negative values) the box size by adding delta to both width and height. If preserve
* center is set, the box will expand around the center, otherwise the top left corner will remain in place. */
void Box::expand(float delta, bool preserve_center)
{
expand({delta, delta}, preserve_center);
}
/* Move the box in the x and y plane by delta amount */
void Box::move(const glm::vec2& delta)
{
x += delta.x;
y += delta.y;
}
/* Return a copy of the original box, moved by delta amount in the x and y plane */
Box Box::stamp(const glm::vec2& delta) const
{
Box clone = *this;
clone.move(delta);
return clone;
}
/* Returns true if box fits completely inside container, false otherwise. Includes the sides
* of the box, so if the sides are equal, it is considered inside. */
bool Box::fits(const Box& container) const
{
return
!(
((inverted_y() && top() > container.top()) || (!inverted_y() && top() < container.top())) ||
right() > container.right() ||
((inverted_y() && bottom() < container.bottom()) || (!inverted_y() && bottom() > container.bottom())) ||
left() < container.left()
);
}
/* Removes any part of the box that is not inside the passed crop area box */
void Box::crop(const Box& area)
{
if ((inverted_y() && top() > area.top()) || (!inverted_y() && top() < area.top()))
{
top(area.top(), true);
}
if (right() > area.right())
{
right(area.right(), true);
}
if ((inverted_y() && bottom() < area.bottom()) || (!inverted_y() && bottom() > area.bottom()))
{
bottom(area.bottom(), true);
}
if (left() < area.left())
{
left(area.left(), true);
}
}
/* Returns true if point is inside the box, false otherwise. Includes the edges, so points equal to the edges
* will return true. */
bool Box::collide(const glm::vec2& point) const
{
return point.x >= left() && point.x <= right() && point.y >= (inverted_y() ? bottom() : top()) && point.y <= (inverted_y() ? top() : bottom());
}
/* Returns true if the line segment intersects the box, false otherwise. If intersection is passed and there is a
* collision, intersection will be filled with the coordinates of the first intersection found unless the segment
* is fully within the box not touching any edges. */
bool Box::collide(const Segment& segment, glm::vec2* intersection) const
{
/* do the faster check of whether the square represented by the segment diagonal collides to determine
* if we should look more closely */
if (collide(segment.box()))
{
/* check if segment intersects any edges, storing the intersection point if so */
if (segment.intersect({nw(), ne()}, intersection) ||
segment.intersect({ne(), se()}, intersection) ||
segment.intersect({sw(), se()}, intersection) ||
segment.intersect({nw(), sw()}, intersection))
{
return true;
}
/* check if segment is fully inside the box */
else if (collide(segment.start()) && collide(segment.end()))
{
return true;
}
/* otherwise, segment must be outside the box even though its box has collided */
else
{
return false;
}
}
else
{
return false;
}
}
/* Do segment collision with intersection specified by reference rather than pointer */
bool Box::collide(const Segment& segment, glm::vec2& intersection) const
{
return collide(segment, &intersection);
}
/* Return true if box collides with the passed box, false otherwise. If overlap is passed, it is set to
* the box representing the area where the two boxes overlap. */
bool Box::collide(const Box& box, Box* overlap) const
{
float top, bottom, h;
if (inverted_y())
{
top = std::min(this->top(), box.top());
bottom = std::max(this->bottom(), box.bottom());
h = top - bottom;
}
else
{
top = std::max(this->top(), box.top());
bottom = std::min(this->bottom(), box.bottom());
h = bottom - top;
}
float right = std::min(this->right(), box.right());
float left = std::max(this->left(), box.left());
float w = right - left;
bool collide = w > 0 && h > 0;
if (collide && overlap != nullptr)
{
overlap->left(left);
overlap->top(top);
overlap->width(w);
overlap->height(h);
}
return collide;
}
/* Do a box to box collision test with overlap passed by reference instead of pointer */
bool Box::collide(const Box& box, Box& overlap) const
{
return collide(box, &overlap);
}
/* Return the string representation of a Box "{left, top, width, height}" */
std::string Box::string() const
{
std::stringstream output;
output << "{(" << left() << ", " << top() << "), (" << width() << ", " << height() << ")}";
return output.str();
}
/* Feed a string representation of the box to the passed ostream */
std::ostream& std::operator<<(std::ostream& out, const Box& box)
{
out << box.string();
return out;
}