/* /\ +--------------------------------------------------------------+ ____/ \____ /| - 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(left()), static_cast(top()), static_cast(width()), static_cast(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; }