diff --git a/Userland/Libraries/LibGfx/Forward.h b/Userland/Libraries/LibGfx/Forward.h index 3bfd18954e..92fbaff5f8 100644 --- a/Userland/Libraries/LibGfx/Forward.h +++ b/Userland/Libraries/LibGfx/Forward.h @@ -16,6 +16,10 @@ class Emoji; class Font; class GlyphBitmap; class ImageDecoder; + +template +class Line; + class Painter; class Palette; class PaletteImpl; @@ -34,6 +38,9 @@ class Size; template class Rect; +using IntLine = Line; +using FloatLine = Line; + using IntRect = Rect; using FloatRect = Rect; diff --git a/Userland/Libraries/LibGfx/Line.h b/Userland/Libraries/LibGfx/Line.h new file mode 100644 index 0000000000..f9d8176846 --- /dev/null +++ b/Userland/Libraries/LibGfx/Line.h @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2021, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Gfx { + +template +class Line { +public: + Line() { } + + Line(Point a, Point b) + : m_a(a) + , m_b(b) + { + } + + template + Line(U a, U b) + : m_a(a) + , m_b(b) + { + } + + template + explicit Line(Line const& other) + : m_a(other.a()) + , m_b(other.b()) + { + } + + bool intersects(Line const& other) const + { + return intersected(other).has_value(); + } + + Optional> intersected(Line const& other) const + { + auto cross_product = [](Point const& p1, Point const& p2) { + return p1.x() * p2.y() - p1.y() * p2.x(); + }; + auto r = m_b - m_a; + auto s = other.m_b - other.m_a; + auto delta_a = other.m_a - m_a; + auto num = cross_product(delta_a, r); + auto denom = cross_product(r, s); + if (denom == 0) { + if (num == 0) { + // Lines are collinear, check if line ends are touching + if (m_a == other.m_a || m_a == other.m_b) + return m_a; + if (m_b == other.m_a || m_b == other.m_b) + return m_b; + // Check if they're overlapping + if (!(m_b.x() - m_a.x() < 0 && m_b.x() - other.m_a.x() < 0 && other.m_b.x() - m_a.x() && other.m_b.x() - other.m_a.x())) { + // Overlapping + // TODO find center point? + } + if (!(m_b.y() - m_a.y() < 0 && m_b.y() - other.m_a.y() < 0 && other.m_b.y() - m_a.y() && other.m_b.y() - other.m_a.y())) { + // Overlapping + // TODO find center point? + } + return {}; + } else { + // Lines are parallel and not intersecting + return {}; + } + } + auto u = static_cast(num) / static_cast(denom); + if (u < 0.0f || u > 1.0f) { + // Lines are not parallel and don't intersect + return {}; + } + auto t = static_cast(cross_product(delta_a, s)) / static_cast(denom); + if (t < 0.0f || t > 1.0f) { + // Lines are not parallel and don't intersect + return {}; + } + // TODO: round if we're dealing with int + return Point { m_a.x() + static_cast(t * r.x()), m_a.y() + static_cast(t * r.y()) }; + } + + float length() const + { + return m_a.distance_from(m_b); + } + + Point closest_to(Point const& point) const + { + if (m_a == m_b) + return m_a; + auto delta_a = point.x() - m_a.x(); + auto delta_b = point.y() - m_a.y(); + auto delta_c = m_b.x() - m_a.x(); + auto delta_d = m_b.y() - m_a.y(); + auto len_sq = delta_c * delta_c + delta_d * delta_d; + float param = -1.0; + if (len_sq != 0) + param = static_cast(delta_a * delta_c + delta_b * delta_d) / static_cast(len_sq); + if (param < 0) + return m_a; + if (param > 1) + return m_b; + // TODO: round if we're dealing with int + return { static_cast(m_a.x() + param * delta_c), static_cast(m_a.y() + param * delta_d) }; + } + + Line shortest_line_to(Point const& point) const + { + return { closest_to(point), point }; + } + + float distance_to(Point const& point) const + { + return shortest_line_to(point).length(); + } + + Point const& a() const { return m_a; } + Point const& b() const { return m_b; } + + void set_a(Point const& a) { m_a = a; } + void set_b(Point const& b) { m_b = b; } + + String to_string() const; + +private: + Point m_a; + Point m_b; +}; + +template<> +inline String IntLine::to_string() const +{ + return String::formatted("[{},{} -> {}x{}]", m_a.x(), m_a.y(), m_b.x(), m_b.y()); +} + +template<> +inline String FloatLine::to_string() const +{ + return String::formatted("[{},{} {}x{}]", m_a.x(), m_a.y(), m_b.x(), m_b.y()); +} + +} diff --git a/Userland/Libraries/LibGfx/Rect.cpp b/Userland/Libraries/LibGfx/Rect.cpp index 7fbf5022c7..8fe8c1baeb 100644 --- a/Userland/Libraries/LibGfx/Rect.cpp +++ b/Userland/Libraries/LibGfx/Rect.cpp @@ -7,12 +7,183 @@ #include #include #include +#include #include #include #include namespace Gfx { +template + +Rect::RelativeLocation::RelativeLocation(Rect const& base_rect, Rect const& other_rect) +{ + if (base_rect.is_empty() || other_rect.is_empty()) + return; + auto parts = base_rect.shatter(other_rect); + for (auto& part : parts) { + if (part.x() < other_rect.x()) { + if (part.y() < other_rect.y()) + m_top_left = true; + if ((part.y() >= other_rect.y() && part.y() < other_rect.bottom()) || (part.y() <= other_rect.bottom() && part.bottom() > other_rect.y())) + m_left = true; + if (part.y() >= other_rect.bottom() || part.bottom() > other_rect.y()) + m_bottom_left = true; + } + if (part.x() >= other_rect.x() || part.right() > other_rect.x()) { + if (part.y() < other_rect.y()) + m_top = true; + if (part.y() >= other_rect.bottom() || part.bottom() > other_rect.bottom()) + m_bottom = true; + } + if (part.x() >= other_rect.right() || part.right() > other_rect.right()) { + if (part.y() < other_rect.y()) + m_top_right = true; + if ((part.y() >= other_rect.y() && part.y() < other_rect.bottom()) || (part.y() <= other_rect.bottom() && part.bottom() > other_rect.y())) + m_right = true; + if (part.y() >= other_rect.bottom() || part.bottom() > other_rect.y()) + m_bottom_right = true; + } + } +} + +template +Vector, 2> Rect::intersected(Line const& line) const +{ + if (is_empty()) + return {}; + Vector, 2> points; + if (auto point = line.intersected({ top_left(), top_right() }); point.has_value()) + points.append({ point.value().x(), y() }); + if (auto point = line.intersected({ bottom_left(), bottom_right() }); point.has_value()) { + points.append({ point.value().x(), bottom() }); + if (points.size() == 2) + return points; + } + if (height() > 2) { + if (auto point = line.intersected({ { x(), y() + 1 }, { x(), bottom() - 1 } }); point.has_value()) { + points.append({ x(), point.value().y() }); + if (points.size() == 2) + return points; + } + if (auto point = line.intersected({ { right(), y() + 1 }, { right(), bottom() - 1 } }); point.has_value()) + points.append({ right(), point.value().y() }); + } + return points; +} + +template +float Rect::center_point_distance_to(Rect const& other) const +{ + return Line { center(), other.center() }.length(); +} + +template +Vector, 2> Rect::closest_outside_center_points(Rect const& other) const +{ + if (intersects(other)) + return {}; + Line centers_line { center(), other.center() }; + auto points_this = intersected(centers_line); + VERIFY(points_this.size() == 1); + auto points_other = other.intersected(centers_line); + VERIFY(points_other.size() == 1); + return { points_this[0], points_other[0] }; +} + +template +float Rect::outside_center_point_distance_to(Rect const& other) const +{ + auto points = closest_outside_center_points(other); + if (points.is_empty()) + return 0.0; + return Line { points[0], points[0] }.length(); +} + +template +Rect Rect::constrained_to(Rect const& constrain_rect) const +{ + if (constrain_rect.contains(*this)) + return *this; + T move_x = 0, move_y = 0; + if (right() > constrain_rect.right()) + move_x = constrain_rect.right() - right(); + if (bottom() > constrain_rect.bottom()) + move_y = constrain_rect.bottom() - bottom(); + if (x() < constrain_rect.x()) + move_x = x() - constrain_rect.x(); + if (y() < constrain_rect.y()) + move_y = y() - constrain_rect.y(); + auto rect = *this; + if (move_x != 0 || move_y != 0) + rect.translate_by(move_x, move_y); + return rect; +} + +template +Rect Rect::aligned_within(Size const& rect_size, Point const& align_at, TextAlignment alignment) const +{ + if (rect_size.is_empty()) + return {}; + if (!size().contains(rect_size)) + return {}; + if (!contains(align_at)) + return {}; + + Rect rect; + switch (alignment) { + case TextAlignment::TopLeft: + rect = { align_at, rect_size }; + break; + case TextAlignment::CenterLeft: + rect = { { align_at.x(), align_at.y() - rect_size.height() / 2 }, rect_size }; + break; + case TextAlignment::Center: + rect = { { align_at.x() - rect_size.width() / 2, align_at.y() - rect_size.height() / 2 }, rect_size }; + break; + case TextAlignment::CenterRight: + rect = { { align_at.x() - rect_size.width() / 2, align_at.y() }, rect_size }; + break; + case TextAlignment::TopRight: + rect = { { align_at.x() - rect_size.width(), align_at.y() }, rect_size }; + break; + case TextAlignment::BottomLeft: + rect = { { align_at.x(), align_at.y() - rect_size.width() }, rect_size }; + break; + case TextAlignment::BottomRight: + rect = { { align_at.x() - rect_size.width(), align_at.y() - rect_size.width() }, rect_size }; + break; + } + return rect.constrained_to(*this); +} + +template +Point Rect::closest_to(Point const& point) const +{ + if (is_empty()) + return {}; + Optional> closest_point; + float closest_distance = 0.0; + auto check_distance = [&](const Line& line) { + auto point_on_line = line.closest_to(point); + auto distance = Line { point_on_line, point }.length(); + if (!closest_point.has_value() || distance < closest_distance) { + closest_point = point_on_line; + closest_distance = distance; + } + }; + + check_distance({ top_left(), top_right() }); + check_distance({ bottom_left(), bottom_right() }); + if (height() > 2) { + check_distance({ { x(), y() + 1 }, { x(), bottom() - 1 } }); + check_distance({ { right(), y() + 1 }, { right(), bottom() - 1 } }); + } + VERIFY(closest_point.has_value()); + VERIFY(side(closest_point.value()) != Side::None); + return closest_point.value(); +} + template void Rect::intersect(Rect const& other) { diff --git a/Userland/Libraries/LibGfx/Rect.h b/Userland/Libraries/LibGfx/Rect.h index 347d415705..1ec5f2273d 100644 --- a/Userland/Libraries/LibGfx/Rect.h +++ b/Userland/Libraries/LibGfx/Rect.h @@ -444,6 +444,131 @@ public: return intersection(*this, other); } + [[nodiscard]] Vector, 2> intersected(Line const&) const; + [[nodiscard]] float center_point_distance_to(Rect const&) const; + [[nodiscard]] Vector, 2> closest_outside_center_points(Rect const&) const; + [[nodiscard]] float outside_center_point_distance_to(Rect const&) const; + [[nodiscard]] Rect constrained_to(Rect const&) const; + [[nodiscard]] Rect aligned_within(Size const&, Point const&, TextAlignment = TextAlignment::Center) const; + [[nodiscard]] Point closest_to(Point const&) const; + + class RelativeLocation { + friend class Rect; + + RelativeLocation(Rect const& base_rect, Rect const& other_rect); + + public: + RelativeLocation() = default; + + bool top_left() const { return m_top_left; } + bool top() const { return m_top; } + bool top_right() const { return m_top_right; } + bool left() const { return m_left; } + bool right() const { return m_right; } + bool bottom_left() const { return m_bottom_left; } + bool bottom() const { return m_bottom; } + bool bottom_right() const { return m_bottom_right; } + bool anywhere_above() const { return m_top_left || m_top || m_top_right; } + bool anywhere_below() const { return m_bottom_left || m_bottom || m_bottom_right; } + bool anywhere_left() const { return m_top_left || m_left || m_bottom_left; } + bool anywhere_right() const { return m_top_right || m_right || m_bottom_right; } + + private: + bool m_top_left : 1 { false }; + bool m_top : 1 { false }; + bool m_top_right : 1 { false }; + bool m_left : 1 { false }; + bool m_right : 1 { false }; + bool m_bottom_left : 1 { false }; + bool m_bottom : 1 { false }; + bool m_bottom_right : 1 { false }; + }; + [[nodiscard]] RelativeLocation relative_location_to(Rect const& other) const + { + return RelativeLocation(*this, other); + } + + enum class Side { + None = 0, + Left, + Top, + Right, + Bottom + }; + [[nodiscard]] Side side(Point const& point) const + { + if (is_empty()) + return Side::None; + if (point.y() == y() || point.y() == bottom()) + return (point.x() >= x() && point.x() <= right()) ? (point.y() == y() ? Side::Top : Side::Bottom) : Side::None; + if (point.x() == x() || point.x() == right()) + return (point.y() > y() && point.y() < bottom()) ? (point.x() == x() ? Side::Left : Side::Right) : Side::None; + return Side::None; + } + + [[nodiscard]] Rect rect_on_side(Side side, Rect const& other) const + { + switch (side) { + case Side::None: + break; + case Side::Left: + // Return the area in other that is to the left of this rect + if (other.x() < x()) { + if (other.right() >= x()) + return { other.location(), { x() - other.x(), other.height() } }; + else + return other; + } + break; + case Side::Top: + // Return the area in other that is above this rect + if (other.y() < y()) { + if (other.bottom() >= y()) + return { other.location(), { other.width(), y() - other.y() } }; + else + return other; + } + break; + case Side::Right: + // Return the area in other that is to the right of this rect + if (other.right() >= x()) { + if (other.x() <= right()) + return { { right() + 1, other.y() }, { other.width() - (right() - other.x()), other.height() } }; + else + return other; + } + break; + case Side::Bottom: + // Return the area in other that is below this rect + if (other.bottom() >= y()) { + if (other.y() <= bottom()) + return { { other.x(), bottom() + 1 }, { other.width(), other.height() - (bottom() - other.y()) } }; + else + return other; + } + break; + } + return {}; + } + + [[nodiscard]] bool is_adjacent(Rect const& other) const + { + if (is_empty() || other.is_empty()) + return false; + if (intersects(other)) + return false; + if (other.x() + other.width() == x() || other.x() == x() + width()) + return max(top(), other.top()) <= min(bottom(), other.bottom()); + if (other.y() + other.height() == y() || other.y() == y() + height()) + return max(left(), other.left()) <= min(right(), other.right()); + return false; + } + + [[nodiscard]] static Rect centered_at(Point const& point, Size const& size) + { + return { { point.x() - size.width() / 2, point.y() - size.height() / 2 }, size }; + } + [[nodiscard]] Rect united(Rect const&) const; [[nodiscard]] Point top_left() const { return { left(), top() }; }