diff --git a/README.md b/README.md
index 9af3fe9..3130acb 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,8 @@ In progress...
## Codemap
-- [`utility`](robocin/utility): a collection of utility and helper code.
+- [`geometry`](robocin/geometry): a collection of geometric classes;
+- [`utility`](robocin/utility): a collection of utility and helper code;
diff --git a/robocin/CMakeLists.txt b/robocin/CMakeLists.txt
index f8474db..da17b0f 100644
--- a/robocin/CMakeLists.txt
+++ b/robocin/CMakeLists.txt
@@ -1 +1,2 @@
add_subdirectory(utility)
+add_subdirectory(geometry)
diff --git a/robocin/geometry/CMakeLists.txt b/robocin/geometry/CMakeLists.txt
new file mode 100644
index 0000000..09bc3ac
--- /dev/null
+++ b/robocin/geometry/CMakeLists.txt
@@ -0,0 +1,12 @@
+robocin_cpp_library(
+ NAME point2d
+ HDRS point2d.h
+ SRCS point2d.cpp
+)
+
+robocin_cpp_test(
+ NAME point2d_test
+ HDRS ../utility/internal/test/epsilon_injector.h
+ SRCS point2d_test.cpp
+ DEPS point2d
+)
diff --git a/robocin/geometry/README.md b/robocin/geometry/README.md
new file mode 100644
index 0000000..25bb58a
--- /dev/null
+++ b/robocin/geometry/README.md
@@ -0,0 +1,203 @@
+# geometry
+
+A collection of geometric classes.
+
+## Table of Contents
+
+- [Point2D](#point2d)
+
+
+
+## [`Point2D`](point2d.h)
+
+The `Point2D` templated struct represents a 2-dimensional point (vector) in a Cartesian coordinate system. It provides
+various methods and operators for manipulating and performing calculations with 2d-points.
+
+### Member Types
+
+- `value_type`: The type of the point's coordinates;
+- `reference`: A reference to `value_type`;
+- `size_type`: An unsigned integer type used for size and indexing;
+- `iterator`: An iterator type for iterating over mutable `Point2D` objects;
+- `const_iterator`: An iterator type for iterating over const `Point2D` objects;
+- `reverse_iterator`: A reverse iterator type for reverse iteration over mutable `Point2D` objects;
+- `const_reverse_iterator`: A reverse iterator type for reverse iteration over const `Point2D` objects;
+
+### Static Constructors
+
+- `static Point2D origin()`: Returns a point representing the origin (0, 0);
+- `static Point2D fromPolar(value_type angle) requires(std::floating_point)`: Creates a point from polar
+ coordinates with the given angle;
+- `static Point2D fromPolar(value_type angle, value_type length) requires(std::floating_point)`: Creates a
+ point from polar coordinates with the given angle and length;
+
+### Constructors
+
+- `Point2D()`: Default constructor. Initializes the point with coordinates (0, 0);
+- `Point2D(const Point2D& point)`: Creates a new point by copying the coordinates of another point;
+- `Point2D(Point2D&& point)`: Creates a new point by moving the coordinates of another point;
+- `Point2D(value_type x, value_type y)`: Initializes the point with the specified coordinates;
+- `explicit Point2D(const OtherStructPoint auto& point)`: Initializes the point from coordinates (`point.x`, `point.y`);
+- `explicit Point2D(const OtherClassPoint auto& point)`: Initializes the point from
+ coordinates (`point.x()`, `point.y()`);
+- `explicit Point2D(const std::pair& pair)`: Constructor. Initializes the point with the coordinates from
+ a `std::pair`;
+
+### Assignment Operators
+
+- `constexpr Point2D& operator=(const Point2D& other) = default`: Copy assignment operator. Assigns the coordinates of
+ another point to this point;
+- `constexpr Point2D& operator=(Point2D&& other) noexcept = default`: Move assignment operator. Moves the coordinates of
+ another point to this point;
+
+### Validators
+
+- `bool isNull() const`: Checks if the point (vector) is null (coordinates are both zero). Returns `true` if the point
+ is null, `false` otherwise;
+
+### Arithmetic-Assignment Operators
+
+- `Point2D& operator+=(const Point2D& other)`: Adds the coordinates of another point to this point and updates its
+ value. Returns a reference to the modified point;
+- `Point2D& operator-=(const Point2D& other)`: Subtracts the coordinates of another point from this point and updates
+ its value. Returns a reference to the modified point;
+- `Point2D& operator*=(value_type factor)`: Multiplies the coordinates of this point by a scalar factor and updates its
+ value. Returns a reference to the modified point;
+- `Point2D& operator/=(value_type factor)`: Divides the coordinates of this point by a scalar factor and updates its
+ value. Returns a reference to the modified point;
+
+### Arithmetic Operators
+
+- `Point2D operator+(const Point2D& other) const`: Addition operator that returns the sum of two points;
+- `Point2D operator-(const Point2D& other) const`: Subtraction operator that returns the difference between two points;
+- `Point2D operator*(value_type factor) const`: Multiplication operator that scales the point by a factor;
+- `Point2D operator/(value_type factor) const`: Division operator that scales the point by the inverse of a factor;
+
+### Arithmetic Friend Operators
+
+- `friend Point2D operator*(value_type factor, const Point2D& point)`: Friend operator that allows multiplication of a
+ point by a factor in the reverse order;
+
+### Sign Operators
+
+- `Point2D operator+() const`: Unary plus operator that returns the point itself;
+- `Point2D operator-() const`: Unary minus operator that returns the negation of the point;
+
+### Comparison Operators
+
+- `bool operator==(const Point2D& other) const`: Equality operator that checks if two points are equal;
+- `auto operator<=>(const Point2D& other) const`: Three-way comparison operator that compares
+ two points and returns their relative ordering;
+
+### Swap
+
+- `void swap(Point2D& other) noexcept`: Swaps the contents of the current point with another point;
+
+### Geometry
+
+- `value_type dot(const Point2D& other) const`: Computes the dot product of the current point and another point;
+- `value_type cross(const Point2D& other) const`: Computes the cross product of the current point and another point;
+- `value_type manhattanLength() const`: Computes the Manhattan distance between the origin and the current point;
+- `value_type manhattanDistTo(const Point2D& other) const`: Computes the Manhattan distance between the current point
+ and another point;
+- `value_type lengthSquared() const`: Computes the square of the Euclidean length of the current point;
+- `std::floating_point auto length() const`: Computes the Euclidean length of the current point;
+- `std::floating_point auto norm() const`: Computes the Euclidean length of the current point;
+- `value_type distSquaredTo(const Point2D& other) const`: Computes the square of the Euclidean distance between the
+ current point and another point;
+- `std::floating_point auto distTo(const Point2D& other) const`: Computes the Euclidean distance between the current
+ point and another point;
+- `std::floating_point auto angle() const`: Computes the angle (in radians) [-PI , +PI] between the positive x-axis and
+ the origin to the current point;
+- `std::floating_point auto angleTo(const Point2D& other) const`: Computes the angle (in radians) [-PI , +PI] between
+ the origin to the current point and the origin to another point;
+
+### Rotations
+
+- `void rotateCW90() &`: Rotates the current point 90 degrees clockwise;
+- `Point2D rotatedCW90() &&`: Returns a new point obtained by rotating the current point 90 degrees clockwise;
+- `Point2D rotatedCW90() const&`: Returns a new point obtained by rotating a copy of the current point 90 degrees
+ clockwise;
+
+
+- `void rotateCCW90() &`: Rotates the current point 90 degrees counterclockwise;
+- `Point2D rotatedCCW90() &&`: Returns a new point obtained by rotating the current point 90 degrees counterclockwise;
+- `Point2D rotatedCCW90() const&`: Returns a new point obtained by rotating a copy of the current point 90 degrees
+ counterclockwise;
+
+
+- `void rotateCW(value_type t) &`: Rotates the current point clockwise by a specified angle `t` (in radians);
+- `Point2D rotatedCW(value_type t) &&`: Returns a new point obtained by rotating the current point clockwise by a
+ specified angle `t` (in radians);
+- `Point2D rotatedCW(value_type t) const&`: Returns a new point obtained by rotating a copy of the current point
+ clockwise by a specified angle `t` (in radians);
+
+
+- `void rotateCCW(value_type t) &`: Rotates the current point counterclockwise by a specified angle `t` (in radians);
+- `Point2D rotatedCCW(value_type t) &&`: Returns a new point obtained by rotating the current point counterclockwise by
+ a specified angle `t` (in radians);
+- `Point2D rotatedCCW(value_type t) const&`: Returns a new point obtained by rotating a copy of the current point
+ counterclockwise by a specified angle `t` (in radians);
+
+### Resizing and Normalization
+
+- `void resize(value_type t) &`: Resizes the current point by a factor `t`;
+- `Point2D resized(value_type t) &&`: Returns a new point obtained by resizing the current point by a factor `t`;
+- `Point2D resized(value_type t) const&`: Returns a new point obtained by resizing a copy of the current point by a
+ factor `t`;
+
+- `void normalize() &`: Normalizes the current point to have unit length.
+
+> **Note:** The normalization differs for real and integer numbers, being equivalent to `resize(1)` for real numbers,
+> and the division of coordinates by their gcd for integer numbers.
+
+- `Point2D normalized() &&`: Returns a new point obtained by normalizing the current point to have unit length;
+- `Point2D normalized() const&`: Returns a new point obtained by normalizing a copy of the current point to have unit
+ length;
+
+
+- `void axesNormalize() &`: Normalizes the current point to have coordinates of -1, 0, or 1;
+- `Point2D axesNormalized() &&`: Returns a new point obtained by normalizing the current point to have coordinates of
+ -1, 0, or 1;
+- `Point2D axesNormalized() const&`: Returns a new point obtained by normalizing a copy of the current point to have
+ coordinates of -1, 0, or 1;
+
+### Array-like
+
+- `static size_type size()`: Returns the size of the point (always 2);
+- `reference operator[](size_type pos)`: Provides access to the elements of the point using the subscript operator;
+- `value_type operator[](size_type pos) const`: Provides access to the elements of the point using the subscript
+ operator;
+
+### Iterators
+
+The following iterator methods provide support for iterating over the elements of the point:
+
+- `iterator begin() noexcept`;
+- `const_iterator begin() const noexcept`;
+- `iterator end() noexcept`;
+- `const_iterator end() const noexcept`;
+- `reverse_iterator rbegin() noexcept`;
+- `const_reverse_iterator rbegin() const noexcept`;
+- `reverse_iterator rend() noexcept`;
+- `const_reverse_iterator rend() const noexcept`;
+- `const_iterator cbegin() const noexcept`;
+- `const_iterator cend() const noexcept`;
+- `const_reverse_iterator crbegin() const noexcept`;
+- `const_reverse_iterator crend() const noexcept`;
+
+### Input/Output Operators
+
+The following operators allow input and output of `Point2D` objects:
+
+- `std::istream& operator>>(std::istream& is, Point2D& point)`: Reads a `Point2D` object from an input stream.
+- `std::ostream& operator<<(std::ostream& os, const Point2D& point)`: Writes a `Point2D` object to an output stream.
+
+### Deduction Guides
+
+The following deduction guides are provided:
+
+- `Point2D() -> Point2D`: Constructs a `Point2D` with `double` coordinates when no arguments are provided;
+- `Point2D(T, U) -> Point2D>`: This deduction guide ensures that when constructing a `Point2D`
+ with different coordinate types `T` and `U`, the resulting `Point2D` type will have a coordinate type that is the
+ common type of `T` and `U`;
diff --git a/robocin/geometry/internal/point2d_internal.h b/robocin/geometry/internal/point2d_internal.h
new file mode 100644
index 0000000..a83e12f
--- /dev/null
+++ b/robocin/geometry/internal/point2d_internal.h
@@ -0,0 +1,109 @@
+//
+// Created by José Cruz on 24/02/23.
+// Copyright (c) 2023 RobôCIn.
+//
+
+#ifndef ROBOCIN_GEOMETRY_POINT2D_INTERNAL_H
+#define ROBOCIN_GEOMETRY_POINT2D_INTERNAL_H
+
+#include
+#include
+
+namespace robocin::point2d_internal {
+
+template
+concept StructPoint = requires(PT point) {
+ point.x;
+ point.y;
+};
+
+template
+concept ClassPoint = requires(PT point) {
+ point.x();
+ point.y();
+};
+
+template
+class iterator {
+ using point_type = PT;
+ using point_pointer = point_type*;
+
+ template
+ using dependent_const_t = std::conditional_t, const T, T>;
+
+ public:
+ // NOLINTNEXTLINE(readability-identifier-naming)
+ using iterator_category = std::random_access_iterator_tag;
+ using difference_type = std::ptrdiff_t;
+ using value_type = dependent_const_t;
+ using pointer = value_type*;
+ using reference = value_type&;
+
+ // Constructors ----------------------------------------------------------------------------------
+ constexpr iterator() = default;
+ constexpr iterator(const iterator&) = default;
+ constexpr iterator(iterator&&) noexcept = default;
+ constexpr iterator(point_pointer ptr, difference_type index) : ptr_(ptr), index_(index) {}
+
+ // Assignment operators --------------------------------------------------------------------------
+ constexpr iterator& operator=(const iterator&) = default;
+ constexpr iterator& operator=(iterator&&) noexcept = default;
+
+ // Destructor ------------------------------------------------------------------------------------
+ constexpr ~iterator() = default;
+
+ // Dereference operators -------------------------------------------------------------------------
+ constexpr reference operator*() const {
+ switch (index_) {
+ case 0: return ptr_->x;
+ case 1: return ptr_->y;
+ default: throw std::out_of_range("Point2D::iterator operator*: index out of range.");
+ }
+ }
+
+ constexpr reference operator[](difference_type n) const {
+ switch (index_ + n) {
+ case 0: return ptr_->x;
+ case 1: return ptr_->y;
+ default: throw std::out_of_range("Point2D::iterator operator[]: index out of range.");
+ }
+ }
+
+ // Arithmetic-assignment operators ---------------------------------------------------------------
+ constexpr iterator& operator+=(difference_type n) { return index_ += n, *this; }
+ constexpr iterator& operator-=(difference_type n) { return index_ -= n, *this; }
+
+ // Arithmetic operators --------------------------------------------------------------------------
+ constexpr iterator operator+(difference_type n) const { return iterator(*this) += n; }
+ constexpr iterator operator-(difference_type n) const { return iterator(*this) -= n; }
+
+ constexpr difference_type operator-(const iterator& other) const { return index_ - other.index_; }
+
+ // Arithmetic friend operator --------------------------------------------------------------------
+ friend constexpr iterator operator+(difference_type n, const iterator& it) { return it + n; }
+
+ // Increment and decrement operators -------------------------------------------------------------
+ constexpr iterator& operator++() { return ++index_, *this; }
+ constexpr iterator operator++(int) { // NOLINT(cert-dcl21-cpp)
+ iterator result = *this;
+ return ++index_, result;
+ }
+
+ constexpr iterator& operator--() { return --index_, *this; }
+ constexpr iterator operator--(int) { // NOLINT(cert-dcl21-cpp)
+ iterator result = *this;
+ return --index_, result;
+ }
+
+ // Comparison operators --------------------------------------------------------------------------
+ inline constexpr bool operator==(const iterator& other) const = default;
+ inline constexpr auto operator<=>(const iterator& other) const = default;
+
+ private:
+ point_pointer ptr_{nullptr};
+ difference_type index_{0};
+};
+
+} // namespace robocin::point2d_internal
+
+#endif // ROBOCIN_GEOMETRY_POINT2D_INTERNAL_H
diff --git a/robocin/geometry/point2d.cpp b/robocin/geometry/point2d.cpp
new file mode 100644
index 0000000..10fe031
--- /dev/null
+++ b/robocin/geometry/point2d.cpp
@@ -0,0 +1,17 @@
+//
+// Created by José Cruz on 24/02/23.
+// Copyright (c) 2023 RobôCIn.
+//
+
+#include "robocin/geometry/point2d.h"
+
+namespace robocin {
+
+template struct Point2D;
+template struct Point2D;
+template struct Point2D;
+template struct Point2D;
+template struct Point2D;
+template struct Point2D;
+
+} // namespace robocin
diff --git a/robocin/geometry/point2d.h b/robocin/geometry/point2d.h
new file mode 100644
index 0000000..53d952a
--- /dev/null
+++ b/robocin/geometry/point2d.h
@@ -0,0 +1,352 @@
+//
+// Created by José Cruz on 24/02/23.
+// Copyright (c) 2023 RobôCIn.
+//
+
+#ifndef ROBOCIN_GEOMETRY_POINT2D_H
+#define ROBOCIN_GEOMETRY_POINT2D_H
+
+#include "robocin/geometry/internal/point2d_internal.h"
+#include "robocin/utility/fuzzy_compare.h"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace robocin {
+
+template
+struct Point2D {
+ public:
+ // Member types ----------------------------------------------------------------------------------
+ using value_type = T;
+ using reference = value_type&;
+ using size_type = std::size_t;
+ using iterator = point2d_internal::iterator;
+ using const_iterator = point2d_internal::iterator;
+ using reverse_iterator = std::reverse_iterator;
+ using const_reverse_iterator = std::reverse_iterator;
+
+ // Friendships -----------------------------------------------------------------------------------
+ template
+ friend struct Point2D;
+
+ // Members ---------------------------------------------------------------------------------------
+ value_type x, y;
+
+ // Static constructors ---------------------------------------------------------------------------
+ static consteval Point2D origin() { return Point2D(); }
+ static constexpr Point2D fromPolar(value_type angle)
+ requires(std::floating_point)
+ {
+ return Point2D(std::cos(angle), std::sin(angle));
+ }
+ static constexpr Point2D fromPolar(value_type angle, value_type length)
+ requires(std::floating_point)
+ {
+ return Point2D(std::cos(angle) * length, std::sin(angle) * length);
+ }
+
+ // Constructors ----------------------------------------------------------------------------------
+ constexpr Point2D() : x(0), y(0) {}
+ constexpr Point2D(const Point2D& point) = default;
+ constexpr Point2D(Point2D&& point) noexcept = default;
+ // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
+ constexpr Point2D(value_type x, value_type y) : x(x), y(y) {}
+ constexpr explicit Point2D(const point2d_internal::StructPoint auto& point) :
+ x(static_cast(point.x)),
+ y(static_cast(point.y)) {}
+ constexpr explicit Point2D(const point2d_internal::ClassPoint auto& point) :
+ x(static_cast(point.x())),
+ y(static_cast(point.y())) {}
+ template
+ constexpr explicit Point2D(const std::pair& pair) :
+ x(static_cast(pair.first)),
+ y(static_cast(pair.second)) {}
+
+ // Destructor ------------------------------------------------------------------------------------
+ constexpr ~Point2D() = default;
+
+ // Assignment operators --------------------------------------------------------------------------
+ constexpr Point2D& operator=(const Point2D& other) = default;
+ constexpr Point2D& operator=(Point2D&& other) noexcept = default;
+
+ // Validators ------------------------------------------------------------------------------------
+ [[nodiscard]] constexpr bool isNull() const {
+ if constexpr (has_epsilon_v) {
+ return fuzzyIsZero(x) and fuzzyIsZero(y);
+ } else {
+ return x == 0 and y == 0;
+ }
+ }
+
+ // Arithmetic-assignment operators ---------------------------------------------------------------
+ inline constexpr Point2D& operator+=(const Point2D& other) {
+ return x += other.x, y += other.y, *this;
+ }
+ inline constexpr Point2D& operator-=(const Point2D& other) {
+ return x -= other.x, y -= other.y, *this;
+ }
+ inline constexpr Point2D& operator*=(value_type factor) {
+ return x *= factor, y *= factor, *this;
+ }
+ inline constexpr Point2D& operator/=(value_type factor) {
+ return x /= factor, y /= factor, *this;
+ }
+
+ // Arithmetic operators --------------------------------------------------------------------------
+ inline constexpr Point2D operator+(const Point2D& other) const { return Point2D(*this) += other; }
+ inline constexpr Point2D operator-(const Point2D& other) const { return Point2D(*this) -= other; }
+ inline constexpr Point2D operator*(value_type factor) const { return Point2D(*this) *= factor; }
+ inline constexpr Point2D operator/(value_type factor) const { return Point2D(*this) /= factor; }
+
+ // Arithmetic friend operators -------------------------------------------------------------------
+ friend inline constexpr Point2D operator*(value_type factor, const Point2D& point) {
+ return Point2D(factor * point.x, factor * point.y);
+ }
+
+ // Sign operators --------------------------------------------------------------------------------
+ inline constexpr Point2D operator+() const { return *this; }
+ inline constexpr Point2D operator-() const { return Point2D(-x, -y); }
+
+ // Comparison operators --------------------------------------------------------------------------
+ inline constexpr bool operator==(const Point2D& other) const {
+ if constexpr (has_epsilon_v) {
+ return fuzzyCmpEqual(x, other.x) and fuzzyCmpEqual(y, other.y);
+ } else {
+ return x == other.x and y == other.y;
+ }
+ }
+
+ inline constexpr auto operator<=>(const Point2D& other) const {
+ if constexpr (has_epsilon_v) {
+ if (auto x_cmp = fuzzyCmpThreeWay(x, other.x); std::is_neq(x_cmp)) {
+ return x_cmp;
+ }
+ return fuzzyCmpThreeWay(y, other.y);
+ } else {
+ if (auto x_cmp = x <=> other.x; std::is_neq(x_cmp)) {
+ return x_cmp;
+ }
+ return y <=> other.y;
+ }
+ }
+
+ // Swap ------------------------------------------------------------------------------------------
+ constexpr void swap(Point2D& other) noexcept { std::swap(*this, other); }
+
+ // Geometry --------------------------------------------------------------------------------------
+ constexpr value_type dot(const Point2D& other) const { return x * other.x + y * other.y; }
+ constexpr value_type cross(const Point2D& other) const { return x * other.y - y * other.x; }
+
+ constexpr value_type manhattanLength() const { return std::abs(x) + std::abs(y); }
+ constexpr value_type manhattanDistTo(const Point2D& other) const {
+ return std::abs(x - other.x) + std::abs(y - other.y);
+ }
+
+ constexpr value_type lengthSquared() const { return x * x + y * y; }
+ constexpr std::floating_point auto length() const { return std::sqrt(lengthSquared()); }
+ constexpr std::floating_point auto norm() const { return std::sqrt(lengthSquared()); }
+
+ constexpr value_type distSquaredTo(const Point2D& other) const {
+ return (x - other.x) * (x - other.x) + (y - other.y) * (y - other.y);
+ }
+ constexpr std::floating_point auto distTo(const Point2D& other) const {
+ return std::sqrt(distSquaredTo(other));
+ }
+
+ constexpr std::floating_point auto angle() const { return std::atan2(y, x); }
+ constexpr std::floating_point auto angleTo(const Point2D& other) const {
+ return std::atan2(cross(other), dot(other));
+ }
+
+ // Rotations -------------------------------------------------------------------------------------
+
+ constexpr void rotateCW90() & { std::swap(x, y), y = -y; }
+ [[nodiscard]] constexpr Point2D rotatedCW90() && { return rotateCW90(), std::move(*this); }
+ [[nodiscard]] constexpr Point2D rotatedCW90() const& { return Point2D(*this).rotatedCW90(); }
+
+ constexpr void rotateCCW90() & { std::swap(x, y), x = -x; }
+ [[nodiscard]] constexpr Point2D rotatedCCW90() && { return rotateCCW90(), std::move(*this); }
+ [[nodiscard]] constexpr Point2D rotatedCCW90() const& { return Point2D(*this).rotatedCCW90(); }
+
+ constexpr void rotateCW(value_type t) &
+ requires(std::floating_point)
+ {
+ value_type cos = std::cos(t);
+ value_type sin = std::sin(t);
+
+ value_type rx = x * cos + y * sin;
+ value_type ry = -x * sin + y * cos;
+
+ x = rx, y = ry;
+ }
+ [[nodiscard]] constexpr Point2D rotatedCW(value_type t) &&
+ requires(std::floating_point)
+ {
+ return rotateCW(t), std::move(*this);
+ }
+ [[nodiscard]] constexpr Point2D rotatedCW(value_type t) const&
+ requires(std::floating_point)
+ {
+ return Point2D(*this).rotatedCW(t);
+ }
+
+ constexpr void rotateCCW(value_type t) &
+ requires(std::floating_point)
+ {
+ value_type cos = std::cos(t);
+ value_type sin = std::sin(t);
+
+ value_type rx = x * cos - y * sin;
+ value_type ry = x * sin + y * cos;
+
+ x = rx, y = ry;
+ }
+ [[nodiscard]] constexpr Point2D rotatedCCW(value_type t) &&
+ requires(std::floating_point)
+ {
+ return rotateCCW(t), std::move(*this);
+ }
+ [[nodiscard]] constexpr Point2D rotatedCCW(value_type t) const&
+ requires(std::floating_point)
+ {
+ return Point2D(*this).rotatedCCW(t);
+ }
+
+ // Resizing and normalization --------------------------------------------------------------------
+
+ constexpr void resize(value_type t) &
+ requires(std::floating_point)
+ {
+ if constexpr (has_epsilon_v) {
+ if (value_type norm = this->norm(); not fuzzyIsZero(norm)) {
+ x *= t / norm, y *= t / norm;
+ }
+ } else {
+ if (value_type norm = this->norm()) {
+ x *= t / norm, y *= t / norm;
+ }
+ }
+ }
+ [[nodiscard]] constexpr Point2D resized(value_type t) &&
+ requires(std::floating_point)
+ {
+ return resize(t), std::move(*this);
+ }
+ [[nodiscard]] constexpr Point2D resized(value_type t) const&
+ requires(std::floating_point)
+ {
+ return Point2D(*this).resized(t);
+ }
+
+ constexpr void normalize() &
+ requires(std::floating_point)
+ {
+ if constexpr (has_epsilon_v) {
+ if (value_type norm = this->norm(); not fuzzyIsZero(norm)) {
+ x /= norm, y /= norm;
+ }
+ } else {
+ if (value_type norm = this->norm()) {
+ x /= norm, y /= norm;
+ }
+ }
+ }
+ constexpr void normalize() &
+ requires(std::integral)
+ {
+ if (value_type gcd = std::gcd(std::abs(x), std::abs(y))) {
+ x /= gcd, y /= gcd;
+ }
+ }
+ [[nodiscard]] constexpr Point2D normalized() && { return normalize(), std::move(*this); }
+ [[nodiscard]] constexpr Point2D normalized() const& { return Point2D(*this).normalized(); }
+
+ constexpr void axesNormalize() & {
+ if constexpr (has_epsilon_v) {
+ x = fuzzyIsZero(x) ? 0 : (x > 0 ? 1 : -1), y = fuzzyIsZero(y) ? 0 : (y > 0 ? 1 : -1);
+ } else {
+ x = (x == 0) ? 0 : (x > 0 ? 1 : -1), y = (y == 0) ? 0 : (y > 0 ? 1 : -1);
+ }
+ }
+ [[nodiscard]] constexpr Point2D axesNormalized() && { return axesNormalize(), std::move(*this); }
+ [[nodiscard]] constexpr Point2D axesNormalized() const& {
+ return Point2D(*this).axesNormalized();
+ }
+
+ constexpr void transpose() & { std::swap(x, y); }
+ [[nodiscard]] constexpr Point2D transposed() && { return transpose(), std::move(*this); }
+ [[nodiscard]] constexpr Point2D transposed() const& { return Point2D(*this).transposed(); }
+
+ // Array-like ------------------------------------------------------------------------------------
+ [[nodiscard]] static consteval size_type size() { return 2; }
+
+ inline constexpr reference operator[](size_type pos) {
+ switch (pos) {
+ case 0: return x;
+ case 1: return y;
+ default: throw std::out_of_range("Point2D operator[]: index out of range.");
+ }
+ }
+ inline constexpr value_type operator[](size_type pos) const {
+ switch (pos) {
+ case 0: return x;
+ case 1: return y;
+ default: throw std::out_of_range("Point2D operator[]: index out of range.");
+ }
+ }
+
+ // Iterators -------------------------------------------------------------------------------------
+ constexpr iterator begin() noexcept { return iterator{this, /*index=*/0}; }
+ [[nodiscard]] constexpr const_iterator begin() const noexcept {
+ return const_iterator{this, /*index=*/0};
+ }
+ constexpr iterator end() noexcept { return iterator{this, size()}; }
+ [[nodiscard]] constexpr const_iterator end() const noexcept {
+ return const_iterator{this, size()};
+ }
+
+ constexpr reverse_iterator rbegin() noexcept { return reverse_iterator{end()}; }
+ [[nodiscard]] constexpr const_reverse_iterator rbegin() const noexcept {
+ return const_reverse_iterator{end()};
+ }
+ constexpr reverse_iterator rend() noexcept { return reverse_iterator{begin()}; }
+ [[nodiscard]] constexpr const_reverse_iterator rend() const noexcept {
+ return const_reverse_iterator{begin()};
+ }
+
+ [[nodiscard]] constexpr const_iterator cbegin() const noexcept {
+ return const_iterator{this, /*index=*/0};
+ }
+ [[nodiscard]] constexpr const_iterator cend() const noexcept {
+ return const_iterator{this, size()};
+ }
+ [[nodiscard]] constexpr const_reverse_iterator crbegin() const noexcept {
+ return const_reverse_iterator{end()};
+ }
+ [[nodiscard]] constexpr const_reverse_iterator crend() const noexcept {
+ return const_reverse_iterator{begin()};
+ }
+
+ // Input/Output operators ------------------------------------------------------------------------
+ friend inline std::istream& operator>>(std::istream& is, Point2D& point) {
+ return is >> point.x >> point.y;
+ }
+
+ friend inline std::ostream& operator<<(std::ostream& os, const Point2D& point) {
+ return os << "(x = " << point.x << ", y = " << point.y << ")";
+ }
+};
+
+// Deduction guides --------------------------------------------------------------------------------
+Point2D() -> Point2D;
+
+template
+Point2D(T, U) -> Point2D>;
+
+} // namespace robocin
+
+#endif // ROBOCIN_GEOMETRY_POINT2D_H
diff --git a/robocin/geometry/point2d_test.cpp b/robocin/geometry/point2d_test.cpp
new file mode 100644
index 0000000..db7d7ef
--- /dev/null
+++ b/robocin/geometry/point2d_test.cpp
@@ -0,0 +1,752 @@
+//
+// Created by José Cruz on 24/02/23.
+// Copyright (c) 2023 RobôCIn.
+//
+
+#include "robocin/geometry/point2d.h"
+#include "robocin/utility/internal/test/epsilon_injector.h"
+
+#include
+
+namespace robocin {
+namespace {
+
+using ::testing::Test;
+using ::testing::Types;
+
+template
+class Point2DTest : public Test {};
+
+using TestTypes = Types;
+TYPED_TEST_SUITE(Point2DTest, TestTypes);
+
+// Static constructors -----------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, Origin) {
+ const Point2D kPt = Point2D::origin();
+
+ EXPECT_EQ(kPt.x, 0);
+ EXPECT_EQ(kPt.y, 0);
+}
+
+TYPED_TEST(Point2DTest, FromPolarGivenAngle) {
+ if constexpr (std::is_floating_point_v) {
+ static constexpr TypeParam kEpsilon = epsilon_v;
+ static constexpr TypeParam kPi = std::numbers::pi_v;
+ static constexpr TypeParam kSqrt2 = std::numbers::sqrt2_v;
+
+ struct InOut {
+ TypeParam angle;
+ Point2D expected;
+ };
+
+ static constexpr std::array kOctants = {
+ InOut{0.0, {/*x=*/1.0, /*y=*/0.0}}, // 0º
+ InOut{kPi / 4.0, {/*x=*/kSqrt2 / 2.0, /*y=*/kSqrt2 / 2.0}}, // 45º
+ InOut{kPi / 2.0, {/*x=*/0.0, /*y=*/1.0}}, // 90º
+ InOut{3.0 * kPi / 4.0, {/*x=*/-kSqrt2 / 2.0, /*y=*/kSqrt2 / 2.0}}, // 135º
+ InOut{kPi, {/*x=*/-1.0, /*y=*/0.0}}, // 180º
+ InOut{5.0 * kPi / 4.0, {/*x=*/-kSqrt2 / 2.0, /*y=*/-kSqrt2 / 2.0}}, // 225º
+ InOut{3.0 * kPi / 2.0, {/*x=*/0.0, /*y=*/-1.0}}, // 270º
+ InOut{7.0 * kPi / 4.0, {/*x=*/kSqrt2 / 2.0, /*y=*/-kSqrt2 / 2.0}}, // 315º
+ };
+
+ for (auto [angle, expected] : kOctants) {
+ const Point2D kPt = Point2D::fromPolar(angle);
+
+ EXPECT_NEAR(kPt.x, expected.x, kEpsilon);
+ EXPECT_NEAR(kPt.y, expected.y, kEpsilon);
+ }
+ }
+}
+
+TYPED_TEST(Point2DTest, FromPolarGivenAngleAndLength) {
+ if constexpr (std::is_floating_point_v) {
+ static constexpr TypeParam kEpsilon = epsilon_v;
+ static constexpr TypeParam kPi = std::numbers::pi_v;
+ static constexpr TypeParam kSqrt2 = std::numbers::sqrt2_v;
+
+ struct InOut {
+ TypeParam angle;
+ TypeParam length;
+ Point2D expected;
+ };
+
+ static constexpr std::array kOctants = {
+ InOut{0.0, 1.0, {/*x=*/1.0, /*y=*/0.0}}, // 0º
+ InOut{kPi / 4.0, 2.0, {/*x=*/kSqrt2 / 2.0, /*y=*/kSqrt2 / 2.0}}, // 45º
+ InOut{kPi / 2.0, 4.0, {/*x=*/0.0, /*y=*/1.0}}, // 90º
+ InOut{3.0 * kPi / 4.0, 8.0, {/*x=*/-kSqrt2 / 2.0, /*y=*/kSqrt2 / 2.0}}, // 135º
+ InOut{kPi, 16.0, {/*x=*/-1.0, /*y=*/0.0}}, // 180º
+ InOut{5.0 * kPi / 4.0, 32.0, {/*x=*/-kSqrt2 / 2.0, /*y=*/-kSqrt2 / 2.0}}, // 225º
+ InOut{3.0 * kPi / 2.0, 64.0, {/*x=*/0.0, /*y=*/-1.0}}, // 270º
+ InOut{7.0 * kPi / 4.0, 128.0, {/*x=*/kSqrt2 / 2.0, /*y=*/-kSqrt2 / 2.0}}, // 315º
+ };
+
+ for (auto [angle, length, expected] : kOctants) {
+ const Point2D kPt = Point2D::fromPolar(angle, length);
+
+ EXPECT_NEAR(kPt.x, length * expected.x, kEpsilon);
+ EXPECT_NEAR(kPt.y, length * expected.y, kEpsilon);
+ }
+ }
+}
+
+// Constructors ------------------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, DefaultConstructor) {
+ const Point2D kPt;
+
+ EXPECT_EQ(kPt.x, 0);
+ EXPECT_EQ(kPt.y, 0);
+}
+
+TYPED_TEST(Point2DTest, CopyConstructor) {
+ const Point2D kOther(/*x=*/1, /*y=*/2);
+ const Point2D kPt(kOther);
+
+ EXPECT_EQ(kPt.x, kOther.x);
+ EXPECT_EQ(kPt.y, kOther.y);
+}
+
+TYPED_TEST(Point2DTest, MoveConstructor) {
+ Point2D other(/*x=*/1, /*y=*/2);
+ const Point2D kPt(std::move(other));
+
+ EXPECT_EQ(kPt.x, 1);
+ EXPECT_EQ(kPt.y, 2);
+}
+
+TYPED_TEST(Point2DTest, ConstructorGivenXAndY) {
+ const Point2D kPt(/*x=*/1, /*y=*/2);
+
+ EXPECT_EQ(kPt.x, 1);
+ EXPECT_EQ(kPt.y, 2);
+}
+
+TYPED_TEST(Point2DTest, ConstructorGivenStructPoint) {
+ struct OtherPoint2D {
+ TypeParam x;
+ TypeParam y;
+ };
+
+ const OtherPoint2D kOther{.x = 1, .y = 2};
+ const Point2D kPt(kOther);
+
+ EXPECT_EQ(kPt.x, kOther.x);
+ EXPECT_EQ(kPt.y, kOther.y);
+}
+
+TYPED_TEST(Point2DTest, ConstructorGivenClassPoint) {
+ class OtherPoint2D {
+ public:
+ OtherPoint2D(TypeParam x, TypeParam y) : // NOLINT(bugprone-easily-swappable-parameters)
+ x_(x),
+ y_(y) {}
+
+ [[nodiscard]] TypeParam x() const { return x_; }
+ [[nodiscard]] TypeParam y() const { return y_; }
+
+ private:
+ TypeParam x_;
+ TypeParam y_;
+ };
+
+ const OtherPoint2D kOther{/*x=*/1, /*y=*/2};
+ const Point2D kPt(kOther);
+
+ EXPECT_EQ(kPt.x, kOther.x());
+ EXPECT_EQ(kPt.y, kOther.y());
+}
+
+TYPED_TEST(Point2DTest, ConstructorGivenPair) {
+ const std::pair kPair{1, 2};
+ const Point2D kPt(kPair);
+
+ EXPECT_EQ(kPt.x, kPair.first);
+ EXPECT_EQ(kPt.y, kPair.second);
+}
+
+// Validators --------------------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, IsNull) {
+ const Point2D kPt(/*x=*/0, /*y=*/0);
+ EXPECT_TRUE(kPt.isNull());
+
+ const Point2D kPt2(/*x=*/1, /*y=*/2);
+ EXPECT_FALSE(kPt2.isNull());
+}
+
+// Arithmetic assignment operators -----------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, AdditionAssignment) {
+ Point2D pt(/*x=*/1, /*y=*/2);
+ pt += Point2D(/*x=*/3, /*y=*/4);
+
+ EXPECT_EQ(pt.x, 4);
+ EXPECT_EQ(pt.y, 6);
+}
+
+TYPED_TEST(Point2DTest, SubtractionAssignment) {
+ Point2D pt(/*x=*/1, /*y=*/2);
+ pt -= Point2D(/*x=*/3, /*y=*/4);
+
+ EXPECT_EQ(pt.x, -2);
+ EXPECT_EQ(pt.y, -2);
+}
+
+TYPED_TEST(Point2DTest, MultiplicationAssignment) {
+ Point2D pt(/*x=*/1, /*y=*/2);
+ pt *= 3;
+
+ EXPECT_EQ(pt.x, 3);
+ EXPECT_EQ(pt.y, 6);
+}
+
+TYPED_TEST(Point2DTest, DivisionAssignment) {
+ Point2D pt(/*x=*/1, /*y=*/2);
+ pt /= 2;
+
+ EXPECT_EQ(pt.x, static_cast(1) / 2);
+ EXPECT_EQ(pt.y, static_cast(2) / 2);
+}
+
+// Arithmetic operators ----------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, Addition) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2(/*x=*/3, /*y=*/4);
+
+ const Point2D kPt3 = kPt1 + kPt2;
+
+ EXPECT_EQ(kPt3.x, 4);
+ EXPECT_EQ(kPt3.y, 6);
+}
+
+TYPED_TEST(Point2DTest, Subtraction) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2(/*x=*/3, /*y=*/4);
+
+ const Point2D kPt3 = kPt2 - kPt1;
+
+ EXPECT_EQ(kPt3.x, 2);
+ EXPECT_EQ(kPt3.y, 2);
+}
+
+TYPED_TEST(Point2DTest, Multiplication) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = kPt1 * 3;
+
+ EXPECT_EQ(kPt2.x, 3);
+ EXPECT_EQ(kPt2.y, 6);
+
+ const Point2D kPt3 = 3 * kPt1;
+
+ EXPECT_EQ(kPt3.x, 3);
+ EXPECT_EQ(kPt3.y, 6);
+}
+
+TYPED_TEST(Point2DTest, Division) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = kPt1 / 2;
+
+ EXPECT_EQ(kPt2.x, static_cast(1) / 2);
+ EXPECT_EQ(kPt2.y, static_cast(2) / 2);
+}
+
+// Sign operators ----------------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, UnaryPlus) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = +kPt1;
+
+ EXPECT_EQ(kPt2.x, 1);
+ EXPECT_EQ(kPt2.y, 2);
+}
+
+TYPED_TEST(Point2DTest, UnaryMinus) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = -kPt1;
+
+ EXPECT_EQ(kPt2.x, -1);
+ EXPECT_EQ(kPt2.y, -2);
+}
+
+// Equality operators ------------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, Equality) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2(/*x=*/1, /*y=*/2);
+
+ EXPECT_TRUE(kPt1 == kPt2);
+
+ const Point2D kPt3(/*x=*/3, /*y=*/4);
+ EXPECT_FALSE(kPt1 == kPt3);
+}
+
+// Three-way comparison operator -------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, ThreeWayComparison) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2(/*x=*/1, /*y=*/2);
+
+ EXPECT_EQ(kPt1 <=> kPt2, std::strong_ordering::equal);
+
+ const Point2D kPt3(/*x=*/3, /*y=*/4);
+ EXPECT_EQ(kPt1 <=> kPt3, std::strong_ordering::less);
+ EXPECT_EQ(kPt3 <=> kPt1, std::strong_ordering::greater);
+}
+
+// Swap --------------------------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, Swapping) {
+ Point2D pt1(/*x=*/1, /*y=*/2);
+ Point2D pt2(/*x=*/3, /*y=*/4);
+
+ pt1.swap(pt2);
+
+ EXPECT_EQ(pt1.x, 3);
+ EXPECT_EQ(pt1.y, 4);
+ EXPECT_EQ(pt2.x, 1);
+ EXPECT_EQ(pt2.y, 2);
+}
+
+// Geometry ----------------------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, DotProduct) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2(/*x=*/3, /*y=*/4);
+
+ EXPECT_EQ(kPt1.dot(kPt2), 11);
+}
+
+TYPED_TEST(Point2DTest, CrossProduct) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2(/*x=*/3, /*y=*/4);
+
+ EXPECT_EQ(kPt1.cross(kPt2), -2);
+}
+
+TYPED_TEST(Point2DTest, ManhattanLength) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+
+ EXPECT_EQ(kPt1.manhattanLength(), 3);
+}
+
+TYPED_TEST(Point2DTest, ManhattanDistTo) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2(/*x=*/3, /*y=*/4);
+
+ EXPECT_EQ(kPt1.manhattanDistTo(kPt2), 4);
+}
+
+TYPED_TEST(Point2DTest, LengthSquared) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+
+ EXPECT_EQ(kPt1.lengthSquared(), 5);
+}
+
+TYPED_TEST(Point2DTest, Length) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+
+ using point2d_t = Point2D;
+ using length_result_t = std::invoke_result_t;
+
+ static constexpr std::floating_point auto kEpsilon = epsilon_v;
+
+ EXPECT_NEAR(kPt1.length(), std::sqrt(5), kEpsilon);
+}
+
+TYPED_TEST(Point2DTest, Norm) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+
+ using point2d_t = Point2D;
+ using norm_result_t = std::invoke_result_t;
+
+ static constexpr std::floating_point auto kEpsilon = epsilon_v;
+
+ EXPECT_NEAR(kPt1.norm(), std::sqrt(5), kEpsilon);
+}
+
+TYPED_TEST(Point2DTest, DistSquaredTo) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2(/*x=*/3, /*y=*/4);
+
+ EXPECT_EQ(kPt1.distSquaredTo(kPt2), 8);
+}
+
+TYPED_TEST(Point2DTest, DistTo) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2(/*x=*/3, /*y=*/4);
+
+ using point2d_t = Point2D;
+ using dist_to_result_t = std::invoke_result_t;
+
+ static constexpr std::floating_point auto kEpsilon = epsilon_v;
+
+ EXPECT_NEAR(kPt1.distTo(kPt2), std::sqrt(8), kEpsilon);
+}
+
+TYPED_TEST(Point2DTest, Angle) {
+ const Point2D kPt1(/*x=*/0, /*y=*/1);
+
+ using point2d_t = Point2D;
+ using angle_result_t = std::invoke_result_t;
+
+ static constexpr angle_result_t kEpsilon = epsilon_v;
+ static constexpr std::floating_point auto kPi = std::numbers::pi_v;
+
+ EXPECT_NEAR(kPt1.angle(), kPi / 2, kEpsilon);
+}
+
+TYPED_TEST(Point2DTest, AngleTo) {
+ const Point2D kPt1(/*x=*/0, /*y=*/1);
+ const Point2D kPt2(/*x=*/1, /*y=*/0);
+
+ using point2d_t = Point2D;
+ using angle_to_result_t = std::invoke_result_t;
+
+ static constexpr angle_to_result_t kEpsilon = epsilon_v;
+ static constexpr std::floating_point auto kPi = std::numbers::pi_v;
+
+ EXPECT_NEAR(kPt1.angleTo(kPt2), -kPi / 2, kEpsilon);
+}
+
+// Rotations ---------------------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, RotateCW90) {
+ Point2D pt1(/*x=*/1, /*y=*/2);
+ pt1.rotateCW90();
+
+ EXPECT_EQ(pt1.x, 2);
+ EXPECT_EQ(pt1.y, -1);
+}
+
+TYPED_TEST(Point2DTest, RotatedCW90) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = kPt1.rotatedCW90();
+
+ EXPECT_EQ(kPt2.x, 2);
+ EXPECT_EQ(kPt2.y, -1);
+}
+
+TYPED_TEST(Point2DTest, RotateCCW90) {
+ Point2D pt1(/*x=*/1, /*y=*/2);
+ pt1.rotateCCW90();
+
+ EXPECT_EQ(pt1.x, -2);
+ EXPECT_EQ(pt1.y, 1);
+}
+
+TYPED_TEST(Point2DTest, RotatedCCW90) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = kPt1.rotatedCCW90();
+
+ EXPECT_EQ(kPt2.x, -2);
+ EXPECT_EQ(kPt2.y, 1);
+}
+
+TYPED_TEST(Point2DTest, RotateCW) {
+ if constexpr (std::floating_point) {
+ static constexpr std::floating_point auto kEpsilon = epsilon_v;
+ static constexpr std::floating_point auto kPi = std::numbers::pi_v;
+
+ Point2D pt1(/*x=*/1, /*y=*/2);
+ pt1.rotateCW(kPi / 2);
+
+ EXPECT_NEAR(pt1.x, 2, kEpsilon);
+ EXPECT_NEAR(pt1.y, -1, kEpsilon);
+ }
+}
+
+TYPED_TEST(Point2DTest, RotatedCW) {
+ if constexpr (std::floating_point) {
+ static constexpr std::floating_point auto kEpsilon = epsilon_v;
+ static constexpr std::floating_point auto kPi = std::numbers::pi_v;
+
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = kPt1.rotatedCW(kPi / 2);
+
+ EXPECT_NEAR(kPt2.x, 2, kEpsilon);
+ EXPECT_NEAR(kPt2.y, -1, kEpsilon);
+ }
+}
+
+TYPED_TEST(Point2DTest, RotateCCW) {
+ if constexpr (std::floating_point) {
+ static constexpr std::floating_point auto kEpsilon = epsilon_v;
+ static constexpr std::floating_point auto kPi = std::numbers::pi_v;
+
+ Point2D pt1(/*x=*/1, /*y=*/2);
+ pt1.rotateCCW(kPi / 2);
+
+ EXPECT_NEAR(pt1.x, -2, kEpsilon);
+ EXPECT_NEAR(pt1.y, 1, kEpsilon);
+ }
+}
+
+TYPED_TEST(Point2DTest, RotatedCCW) {
+ if constexpr (std::floating_point) {
+ static constexpr std::floating_point auto kEpsilon = epsilon_v;
+ static constexpr std::floating_point auto kPi = std::numbers::pi_v;
+
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = kPt1.rotatedCCW(kPi / 2);
+
+ EXPECT_NEAR(kPt2.x, -2, kEpsilon);
+ EXPECT_NEAR(kPt2.y, 1, kEpsilon);
+ }
+}
+
+// Resizing and Normalization ----------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, Resize) {
+ if constexpr (std::floating_point) {
+ static constexpr TypeParam kResizeFactor = 2;
+ static constexpr std::floating_point auto kEpsilon = epsilon_v;
+
+ Point2D pt1(/*x=*/1, /*y=*/2);
+ pt1.resize(kResizeFactor);
+
+ EXPECT_NEAR(pt1.x, 1.0 * kResizeFactor / std::sqrt(5), kEpsilon);
+ EXPECT_NEAR(pt1.y, 2.0 * kResizeFactor / std::sqrt(5), kEpsilon);
+ }
+}
+
+TYPED_TEST(Point2DTest, Resized) {
+ if constexpr (std::floating_point) {
+ static constexpr TypeParam kResizeFactor = 2;
+ static constexpr std::floating_point auto kEpsilon = epsilon_v;
+
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = kPt1.resized(kResizeFactor);
+
+ EXPECT_NEAR(kPt2.x, 1.0 * kResizeFactor / std::sqrt(5), kEpsilon);
+ EXPECT_NEAR(kPt2.y, 2.0 * kResizeFactor / std::sqrt(5), kEpsilon);
+ }
+}
+
+TYPED_TEST(Point2DTest, NormalizeFloatingPoint) {
+ if constexpr (std::floating_point) {
+ static constexpr std::floating_point auto kEpsilon = epsilon_v;
+
+ Point2D pt1(/*x=*/1, /*y=*/2);
+ pt1.normalize();
+
+ EXPECT_NEAR(pt1.x, 1.0 / std::sqrt(5), kEpsilon);
+ EXPECT_NEAR(pt1.y, 2.0 / std::sqrt(5), kEpsilon);
+ }
+}
+
+TYPED_TEST(Point2DTest, NormalizeIntegral) {
+ if constexpr (std::integral) {
+ Point2D pt1(/*x=*/1, /*y=*/2);
+ pt1.normalize();
+
+ EXPECT_EQ(pt1.x, 1);
+ EXPECT_EQ(pt1.y, 2);
+ }
+}
+
+TYPED_TEST(Point2DTest, NormalizedFloatingPoint) {
+ if constexpr (std::floating_point) {
+ static constexpr std::floating_point auto kEpsilon = epsilon_v;
+
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = kPt1.normalized();
+
+ EXPECT_NEAR(kPt2.x, 1.0 / std::sqrt(5), kEpsilon);
+ EXPECT_NEAR(kPt2.y, 2.0 / std::sqrt(5), kEpsilon);
+ }
+}
+
+TYPED_TEST(Point2DTest, NormalizedIntegral) {
+ if constexpr (std::integral) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = kPt1.normalized();
+
+ EXPECT_EQ(kPt2.x, 1);
+ EXPECT_EQ(kPt2.y, 2);
+ }
+}
+
+TYPED_TEST(Point2DTest, AxesNormalize) {
+ Point2D pt1(/*x=*/1, /*y=*/2);
+ pt1.axesNormalize();
+
+ EXPECT_EQ(pt1.x, 1);
+ EXPECT_EQ(pt1.y, 1);
+}
+
+TYPED_TEST(Point2DTest, AxesNormalized) {
+ const Point2D kPt1(/*x=*/1, /*y=*/2);
+ const Point2D kPt2 = kPt1.axesNormalized();
+
+ EXPECT_EQ(kPt2.x, 1);
+ EXPECT_EQ(kPt2.y, 1);
+}
+
+// Array-like --------------------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, Size) {
+ static_assert(Point2D::size() == 2);
+
+ const Point2D kPt;
+ EXPECT_EQ(kPt.size(), 2);
+}
+
+TYPED_TEST(Point2DTest, DimensionalAccess) {
+ Point2D pt(/*x=*/1, /*y=*/2);
+
+ EXPECT_EQ(pt[0], 1);
+ EXPECT_EQ(pt[1], 2);
+
+ EXPECT_EQ(&(pt[0]), &(pt.x));
+ EXPECT_EQ(&(pt[1]), &(pt.y));
+
+ EXPECT_THROW(pt[2], std::out_of_range);
+
+ const Point2D kPt(/*x=*/3, /*y=*/4);
+
+ EXPECT_EQ(kPt[0], 3);
+ EXPECT_EQ(kPt[1], 4);
+
+ EXPECT_THROW(kPt[2], std::out_of_range);
+
+ static_assert(!std::is_reference_v); // const point returns a copy.
+ static_assert(!std::is_reference_v); // const point returns a copy.
+}
+
+// Iterators ---------------------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, Iterator) {
+ EXPECT_TRUE(std::random_access_iterator::iterator>);
+ EXPECT_TRUE(std::random_access_iterator::const_iterator>);
+ EXPECT_TRUE(std::random_access_iterator::reverse_iterator>);
+ EXPECT_TRUE(std::random_access_iterator::const_reverse_iterator>);
+}
+
+TYPED_TEST(Point2DTest, Begin) {
+ Point2D pt(/*x=*/1, /*y=*/2);
+ EXPECT_EQ(&(*pt.begin()), &(pt.x));
+
+ const Point2D kPt(/*x=*/3, /*y=*/4);
+ EXPECT_EQ(&(*kPt.begin()), &(kPt.x));
+
+ static_assert(std::is_same_v::const_iterator>);
+}
+
+TYPED_TEST(Point2DTest, End) {
+ Point2D pt(/*x=*/1, /*y=*/2);
+ EXPECT_THROW(*pt.end(), std::out_of_range);
+
+ const Point2D kPt(/*x=*/3, /*y=*/4);
+ EXPECT_THROW(*kPt.end(), std::out_of_range);
+
+ static_assert(std::is_same_v::const_iterator>);
+}
+
+TYPED_TEST(Point2DTest, Cbegin) {
+ Point2D pt(/*x=*/1, /*y=*/2); // NOLINT(misc-const-correctness)
+ EXPECT_EQ(&(*pt.cbegin()), &(pt.x));
+
+ const Point2D kPt(/*x=*/3, /*y=*/4);
+ EXPECT_EQ(&(*kPt.cbegin()), &(kPt.x));
+}
+
+TYPED_TEST(Point2DTest, Cend) {
+ Point2D pt(/*x=*/1, /*y=*/2); // NOLINT(misc-const-correctness)
+ EXPECT_THROW(*pt.cend(), std::out_of_range);
+
+ const Point2D kPt(/*x=*/3, /*y=*/4);
+ EXPECT_THROW(*kPt.cend(), std::out_of_range);
+}
+
+TYPED_TEST(Point2DTest, Rbegin) {
+ Point2D pt(/*x=*/1, /*y=*/2);
+ EXPECT_EQ(&(*pt.rbegin()), &(pt.y));
+
+ const Point2D kPt(/*x=*/3, /*y=*/4);
+ EXPECT_EQ(&(*kPt.rbegin()), &(kPt.y));
+
+ static_assert(
+ std::is_same_v::const_reverse_iterator>);
+}
+
+TYPED_TEST(Point2DTest, Rend) {
+ Point2D pt(/*x=*/1, /*y=*/2);
+ EXPECT_THROW(*pt.rend(), std::out_of_range);
+
+ const Point2D kPt(/*x=*/3, /*y=*/4);
+ EXPECT_THROW(*kPt.rend(), std::out_of_range);
+
+ static_assert(
+ std::is_same_v::const_reverse_iterator>);
+}
+
+TYPED_TEST(Point2DTest, Crbegin) {
+ Point2D pt(/*x=*/1, /*y=*/2); // NOLINT(misc-const-correctness)
+ EXPECT_EQ(&(*pt.crbegin()), &(pt.y));
+
+ const Point2D kPt(/*x=*/3, /*y=*/4);
+ EXPECT_EQ(&(*kPt.crbegin()), &(kPt.y));
+}
+
+TYPED_TEST(Point2DTest, Crend) {
+ Point2D pt(/*x=*/1, /*y=*/2); // NOLINT(misc-const-correctness)
+ EXPECT_THROW(*pt.crend(), std::out_of_range);
+
+ const Point2D kPt(/*x=*/3, /*y=*/4);
+ EXPECT_THROW(*kPt.crend(), std::out_of_range);
+}
+
+// Input/Output ------------------------------------------------------------------------------------
+
+TYPED_TEST(Point2DTest, Input) {
+ Point2D pt;
+ std::istringstream iss("1 2");
+ iss >> pt;
+
+ EXPECT_EQ(pt.x, 1);
+ EXPECT_EQ(pt.y, 2);
+}
+
+TYPED_TEST(Point2DTest, Output) {
+ const Point2D kPt(/*x=*/1, /*y=*/2);
+ std::ostringstream oss;
+ oss << kPt;
+
+ EXPECT_EQ(oss.str(), "(x = 1, y = 2)");
+}
+
+// Deduction Guides --------------------------------------------------------------------------------
+
+TEST(Point2DTest, EmptyDeductionGuide) {
+ const Point2D kPt;
+
+ EXPECT_EQ(kPt.x, 0);
+ EXPECT_EQ(kPt.y, 0);
+
+ static_assert(std::is_same_v);
+}
+
+TEST(Point2DTest, ValueDeductionGuide) {
+ static constexpr int kFirstCoord = 1;
+ static constexpr double kSecondCoord = 2.0;
+
+ const Point2D kPt(kFirstCoord, kSecondCoord);
+
+ using common_type_t = std::common_type_t;
+
+ EXPECT_EQ(kPt.x, static_cast(kFirstCoord));
+ EXPECT_EQ(kPt.y, static_cast(kSecondCoord));
+
+ static_assert(std::is_same_v);
+}
+
+} // namespace
+} // namespace robocin