diff --git a/src/zm_box.h b/src/zm_box.h index 971fbc36d..7094f2cd0 100644 --- a/src/zm_box.h +++ b/src/zm_box.h @@ -20,8 +20,10 @@ #ifndef ZM_BOX_H #define ZM_BOX_H +#include "zm_line.h" #include "zm_vector2.h" #include +#include // // Class used for storing a box, which is defined as a region @@ -48,7 +50,26 @@ class Box { return {mid_x, mid_y}; } - bool Contains(const Vector2 &coord) const { + // Get vertices of the box in a counter-clockwise order + std::vector Vertices() const { + return {lo_, {hi_.x_, lo_.y_}, hi_, {lo_.x_, hi_.y_}}; + } + + // Get edges of the box in a counter-clockwise order + std::vector Edges() const { + std::vector edges; + edges.reserve(4); + + std::vector v = Vertices(); + edges.emplace_back(v[0], v[1]); + edges.emplace_back(v[1], v[2]); + edges.emplace_back(v[2], v[3]); + edges.emplace_back(v[3], v[0]); + + return edges; + } + + bool Contains(const Vector2 &coord) const { return (coord.x_ >= lo_.x_ && coord.x_ <= hi_.x_ && coord.y_ >= lo_.y_ && coord.y_ <= hi_.y_); } diff --git a/src/zm_line.h b/src/zm_line.h new file mode 100644 index 000000000..ec09fc48e --- /dev/null +++ b/src/zm_line.h @@ -0,0 +1,64 @@ +/* + * This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef ZONEMINDER_SRC_ZM_LINE_H_ +#define ZONEMINDER_SRC_ZM_LINE_H_ + +#include "zm_vector2.h" + +// Represents a part of a line bounded by two end points +class LineSegment { + public: + LineSegment(Vector2 start, Vector2 end) : start_(start), end_(end) {} + + public: + Vector2 start_; + Vector2 end_; +}; + +// Represents an infinite line +class Line { + public: + Line(Vector2 p1, Vector2 p2) : position_(p1), direction_(p2 - p1) {} + explicit Line(LineSegment segment) : Line(segment.start_, segment.end_) {}; + + bool IsPointLeftOfOrColinear(Vector2 p) const { + int32 det = direction_.Determinant(p - position_); + + return det >= 0; + } + + Vector2 Intersection(Line const &line) const { + int32 det = direction_.Determinant(line.direction_); + + if (det == 0) { + // lines are parallel or overlap, no intersection + return Vector2::Inf(); + } + + Vector2 c = line.position_ - position_; + double t = c.Determinant(line.direction_) / static_cast(det); + + return position_ + direction_ * t; + } + + private: + Vector2 position_; + Vector2 direction_; +}; + +#endif //ZONEMINDER_SRC_ZM_LINE_H_ diff --git a/src/zm_poly.cpp b/src/zm_poly.cpp index fbeacd06b..0bef5a4fa 100644 --- a/src/zm_poly.cpp +++ b/src/zm_poly.cpp @@ -19,6 +19,7 @@ #include "zm_poly.h" +#include "zm_line.h" #include Polygon::Polygon(std::vector vertices) : vertices_(std::move(vertices)) { @@ -74,3 +75,35 @@ bool Polygon::Contains(const Vector2 &coord) const { } return inside; } + +// Clip the polygon to a rectangular boundary box using the Sutherland-Hodgman algorithm +Polygon Polygon::GetClipped(const Box &boundary) { + std::vector clipped_vertices = vertices_; + + for (LineSegment const& clip_edge : boundary.Edges()) { + // convert our line segment to an infinite line + Line clip_line = Line(clip_edge); + + std::vector to_clip = clipped_vertices; + clipped_vertices.clear(); + + for (size_t i = 0; i < to_clip.size(); ++i) { + Vector2 vert1 = to_clip[i]; + Vector2 vert2 = to_clip[(i + 1) % to_clip.size()]; + + bool vert1_left = clip_line.IsPointLeftOfOrColinear(vert1); + bool vert2_left = clip_line.IsPointLeftOfOrColinear(vert2); + + if (vert2_left) { + if (!vert1_left) { + clipped_vertices.push_back(Line(vert1, vert2).Intersection(clip_line)); + } + clipped_vertices.push_back(vert2); + } else if (vert1_left) { + clipped_vertices.push_back(Line(vert1, vert2).Intersection(clip_line)); + } + } + } + + return Polygon(clipped_vertices); +} diff --git a/src/zm_poly.h b/src/zm_poly.h index bbfd2fc0c..36d7048cc 100644 --- a/src/zm_poly.h +++ b/src/zm_poly.h @@ -40,10 +40,6 @@ struct Edge { } }; -// -// Class used for storing a box, which is defined as a region -// defined by two coordinates -// class Polygon { public: Polygon() : area(0) {} @@ -54,18 +50,20 @@ class Polygon { } const Box &Extent() const { return extent; } - int LoX(int p_lo_x) { return extent.LoX(p_lo_x); } - int HiX(int p_hi_x) { return extent.HiX(p_hi_x); } - int LoY(int p_lo_y) { return extent.LoY(p_lo_y); } - int HiY(int p_hi_y) { return extent.HiY(p_hi_y); } + int32 LoX(int p_lo_x) { return extent.LoX(p_lo_x); } + int32 HiX(int p_hi_x) { return extent.HiX(p_hi_x); } + int32 LoY(int p_lo_y) { return extent.LoY(p_lo_y); } + int32 HiY(int p_hi_y) { return extent.HiY(p_hi_y); } - int Area() const { return area; } + int32 Area() const { return area; } const Vector2 &Centre() const { return centre; } bool Contains(const Vector2 &coord) const; + Polygon GetClipped(const Box &boundary); + private: void calcArea(); void calcCentre(); @@ -73,7 +71,7 @@ class Polygon { private: std::vector vertices_; Box extent; - int area; + int32 area; Vector2 centre; }; diff --git a/src/zm_vector2.h b/src/zm_vector2.h index 522eee51e..0daa51d77 100644 --- a/src/zm_vector2.h +++ b/src/zm_vector2.h @@ -21,6 +21,8 @@ #define ZM_VECTOR2_H #include "zm_define.h" +#include +#include // // Class used for storing an x,y pair, i.e. a coordinate/vector @@ -30,6 +32,11 @@ class Vector2 { Vector2() : x_(0), y_(0) {} Vector2(int32 x, int32 y) : x_(x), y_(y) {} + static Vector2 Inf() { + static const Vector2 inf = {std::numeric_limits::max(), std::numeric_limits::max()}; + return inf; + } + static Vector2 Range(const Vector2 &coord1, const Vector2 &coord2) { Vector2 result((coord1.x_ - coord2.x_) + 1, (coord1.y_ - coord2.y_) + 1); return result; @@ -50,6 +57,9 @@ class Vector2 { Vector2 operator-(const Vector2 &rhs) const { return {x_ - rhs.x_, y_ - rhs.y_}; } + Vector2 operator*(double rhs) const { + return {static_cast(std::lround(x_ * rhs)), static_cast(std::lround(y_ * rhs))}; + } Vector2 &operator+=(const Vector2 &rhs) { x_ += rhs.x_; @@ -62,6 +72,11 @@ class Vector2 { return *this; } + // Calculated the determinant of the 2x2 matrix as given by [[x_, y_], [v.x_y, v.y_]] + int32 Determinant(Vector2 const &v) const { + return (x_ * v.y_) - (y_ * v.x_); + } + public: int32 x_; int32 y_; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ea5c24aa5..880a3d3db 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,7 @@ set(TEST_SOURCES zm_comms.cpp zm_crypt.cpp zm_font.cpp + zm_poly.cpp zm_utils.cpp zm_vector2.cpp) diff --git a/tests/zm_box.cpp b/tests/zm_box.cpp index bd47c0bfb..8141bfa54 100644 --- a/tests/zm_box.cpp +++ b/tests/zm_box.cpp @@ -43,6 +43,8 @@ TEST_CASE("Box: construct from lo and hi") { // Should be: // REQUIRE(b.Centre() == Vector2(3, 3)); REQUIRE(b.Centre() == Vector2(4, 4)); + + REQUIRE(b.Vertices() == std::vector{{1, 1}, {5, 1}, {5, 5}, {1, 5}}); } SECTION("contains") { diff --git a/tests/zm_poly.cpp b/tests/zm_poly.cpp new file mode 100644 index 000000000..817b5a32c --- /dev/null +++ b/tests/zm_poly.cpp @@ -0,0 +1,79 @@ +/* + * This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "zm_catch2.h" + +#include "zm_poly.h" + +TEST_CASE("Polygon: default constructor") { + Polygon p; + + REQUIRE(p.Area() == 0); + REQUIRE(p.Centre() == Vector2(0, 0)); +} + +TEST_CASE("Polygon: construct from vertices") { + std::vector vertices{{{0, 0}, {6, 0}, {0, 6}}}; + Polygon p(vertices); + + REQUIRE(p.Area() == 18); + //REQUIRE(p.Centre() == Vector2(2, 2)); + // Mathematically should be: + //REQUIRE(p.Extent().Size() == Vector2(6, 6)); + REQUIRE(p.Extent().Size() == Vector2(7, 7)); +} + +TEST_CASE("Polygon: clipping") { + // This a concave polygon in a shape resembling a "W" + std::vector v = { + {3, 1}, + {5, 1}, + {6, 3}, + {7, 1}, + {9, 1}, + {10, 8}, + {8, 8}, + {7, 5}, + {5, 5}, + {4, 8}, + {2, 8} + }; + + Polygon p(v); + + REQUIRE(p.GetVertices().size() == 11); + REQUIRE(p.Extent().Size() == Vector2(9, 8)); + // should be: + // REQUIRE(p.Extent().Size() == Vector2(8, 7)); + // related to Vector2::Range + + SECTION("boundary box larger than polygon") { + Polygon c = p.GetClipped(Box({1, 0}, {11, 9})); + + REQUIRE(c.GetVertices().size() == 11); + REQUIRE(c.Extent().Size() == Vector2(9, 8)); + } + + SECTION("boundary box smaller than polygon") { + Polygon c = p.GetClipped(Box({2, 4}, {10, 7})); + + REQUIRE(c.GetVertices().size() == 8); + REQUIRE(c.Extent().Size() == Vector2(9, 4)); + // should be: + // REQUIRE(c.Extent().Size() == Vector2(8, 3)); + } +} diff --git a/tests/zm_vector2.cpp b/tests/zm_vector2.cpp index 23930f41a..625420486 100644 --- a/tests/zm_vector2.cpp +++ b/tests/zm_vector2.cpp @@ -79,4 +79,16 @@ TEST_CASE("Vector2: arithmetic operators") { c -= {1, 2}; REQUIRE(c == Vector2(0, -1)); } + + SECTION("scalar multiplication") { + c = c * 2; + REQUIRE(c == Vector2(2, 2)); + } +} + +TEST_CASE("Vector2: determinate") { + Vector2 v(1, 1); + REQUIRE(v.Determinant({0, 0}) == 0); + REQUIRE(v.Determinant({1, 1}) == 0); + REQUIRE(v.Determinant({1, 2}) == 1); }