From 499c33ae0cf8892dfdf851c178b2b9f7a8bfb79f Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 13 Jun 2021 06:13:06 -0600 Subject: [PATCH] LibGfx: Add a Line class and a Rect::RelativeLocation class These helpers will be useful in preparation for supporting multiple displays, e.g. to measure distances to other screens or figure out where rectangles are located relative to each other. --- Userland/Libraries/LibGfx/Forward.h | 7 ++ Userland/Libraries/LibGfx/Line.h | 155 +++++++++++++++++++++++++ Userland/Libraries/LibGfx/Rect.cpp | 171 ++++++++++++++++++++++++++++ Userland/Libraries/LibGfx/Rect.h | 125 ++++++++++++++++++++ 4 files changed, 458 insertions(+) create mode 100644 Userland/Libraries/LibGfx/Line.h 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() }; }