diff --git a/src/commands/modifypointscommand.cpp b/src/commands/modifypointscommand.cpp index 1ed9cacf2..213aa5faf 100644 --- a/src/commands/modifypointscommand.cpp +++ b/src/commands/modifypointscommand.cpp @@ -5,6 +5,7 @@ #include "path/path.h" #include "path/pathpoint.h" #include "path/pathvector.h" +#include "path/pathview.h" namespace omm { @@ -116,6 +117,8 @@ void AbstractPointsCommand::remove() m_path_object.scene()->update_tool(); } +AbstractPointsCommand::~AbstractPointsCommand() = default; + AddPointsCommand::AddPointsCommand(PathObject& path_object, std::deque&& added_points) : AbstractPointsCommand(static_label(), path_object, std::move(added_points)) { @@ -166,6 +169,8 @@ AbstractPointsCommand::OwnedLocatedPath::OwnedLocatedPath(std::unique_ptr assert(m_owned_path != nullptr); } +AbstractPointsCommand::OwnedLocatedPath::~OwnedLocatedPath() = default; + PathView AbstractPointsCommand::OwnedLocatedPath::insert_into(PathVector& path_vector) { if (m_path == nullptr) { @@ -185,6 +190,7 @@ bool operator<(const AbstractPointsCommand::OwnedLocatedPath& a, return std::tuple{ola.m_path, ola.m_owned_path.get(), ola.m_index}; }; + // NOLINTNEXTLINE(modernize-use-nullptr) return as_tuple(a) < as_tuple(b); } diff --git a/src/commands/modifypointscommand.h b/src/commands/modifypointscommand.h index e175a8364..03c8d8c6e 100644 --- a/src/commands/modifypointscommand.h +++ b/src/commands/modifypointscommand.h @@ -3,6 +3,7 @@ #include "commands/command.h" #include #include +#include "path/pathview.h" namespace omm { @@ -37,6 +38,11 @@ class AbstractPointsCommand : public Command public: explicit OwnedLocatedPath(Path* path, std::size_t index, std::deque>&& points); explicit OwnedLocatedPath(std::unique_ptr path); + ~OwnedLocatedPath(); + OwnedLocatedPath(OwnedLocatedPath&& other) = default; + OwnedLocatedPath& operator=(OwnedLocatedPath&& other) = default; + OwnedLocatedPath(const OwnedLocatedPath& other) = delete; + OwnedLocatedPath& operator=(const OwnedLocatedPath& other) = delete; PathView insert_into(PathVector& path_vector); friend bool operator<(const OwnedLocatedPath& a, const OwnedLocatedPath& b); @@ -58,6 +64,13 @@ class AbstractPointsCommand : public Command void remove(); [[nodiscard]] Scene& scene() const; + ~AbstractPointsCommand() override; + +public: + AbstractPointsCommand(const AbstractPointsCommand&) = delete; + AbstractPointsCommand(AbstractPointsCommand&&) = delete; + AbstractPointsCommand& operator=(const AbstractPointsCommand&) = delete; + AbstractPointsCommand& operator=(AbstractPointsCommand&&) = delete; private: PathObject& m_path_object; diff --git a/src/disjointset.h b/src/disjointset.h index c392466e7..72f68c1bb 100644 --- a/src/disjointset.h +++ b/src/disjointset.h @@ -95,6 +95,12 @@ template class DisjointSetForest protected: std::deque m_forest; + void remove_empty_sets() + { + m_forest.erase(std::remove_if(m_forest.begin(), m_forest.end(), [](const auto& set) { + return set.empty(); + }), m_forest.end()); + } }; template void swap(DisjointSetForest& a, DisjointSetForest& b) noexcept diff --git a/src/mainwindow/pathactions.cpp b/src/mainwindow/pathactions.cpp index 1418835c9..9fa4604af 100644 --- a/src/mainwindow/pathactions.cpp +++ b/src/mainwindow/pathactions.cpp @@ -290,12 +290,34 @@ void invert_selection(Application& app) void select_connected_points(Application& app) { - foreach_subpath(app, [](const auto* segment) { - const auto points = segment->points(); + std::set selected_paths; + foreach_subpath(app, [&selected_paths](const auto* path) { + const auto points = path->points(); if (std::any_of(points.begin(), points.end(), std::mem_fn(&PathPoint::is_selected))) { - std::for_each(points.begin(), points.end(), [](auto* point) { point->set_selected(true); }); + selected_paths.insert(path); } }); + + for (bool selected_paths_changed = true; selected_paths_changed;) { + selected_paths_changed = false; + for (const auto* path : selected_paths) { + for (auto* point : path->points()) { + for (auto* joined_point : point->joined_points()) { + const auto& other_path = joined_point->path(); + if (const auto [_, was_inserted] = selected_paths.insert(&other_path); was_inserted) { + selected_paths_changed = true; + } + } + } + } + } + + for (const auto* path : selected_paths) { + for (auto* point : path->points()) { + point->set_selected(true); + } + } + Q_EMIT app.mail_box().scene_appearance_changed(); } diff --git a/src/objects/object.cpp b/src/objects/object.cpp index f8d9b21ef..4740a995f 100644 --- a/src/objects/object.cpp +++ b/src/objects/object.cpp @@ -666,25 +666,26 @@ void Object::draw_object(Painter& renderer, const Style& style, const PainterOptions& options) const { + options.object_id = id(); if (QPainter* painter = renderer.painter; painter != nullptr && is_active()) { const auto& path_vector = this->path_vector(); const auto faces = path_vector.faces(); const auto& outline = path_vector.outline(); if (!faces.empty() || !outline.isEmpty()) { - renderer.set_style(style, *this, options); - auto& painter = *renderer.painter; - - for (const auto& face : faces) { - painter.save(); - painter.setPen(Qt::NoPen); - painter.drawPath(face); - painter.restore(); + + for (std::size_t f = 0; f < faces.size(); ++f) { + options.path_id = f; + renderer.set_style(style, *this, options); + painter->save(); + painter->setPen(Qt::NoPen); + painter->drawPath(faces.at(f)); + painter->restore(); } - painter.save(); + painter->save(); renderer.painter->setBrush(Qt::NoBrush); - painter.drawPath(outline); - painter.restore(); + painter->drawPath(outline); + painter->restore(); const auto marker_color = style.property(Style::PEN_COLOR_KEY)->value(); const auto width = style.property(Style::PEN_WIDTH_KEY)->value(); diff --git a/src/path/CMakeLists.txt b/src/path/CMakeLists.txt index 27c62eef6..2be17df3d 100644 --- a/src/path/CMakeLists.txt +++ b/src/path/CMakeLists.txt @@ -7,10 +7,12 @@ target_sources(libommpfritt PRIVATE graph.h lib2geomadapter.cpp lib2geomadapter.h - pathvector.h - pathvector.cpp pathpoint.cpp pathpoint.h path.cpp path.h + pathvector.cpp + pathvector.h + pathview.cpp + pathview.h ) diff --git a/src/path/edge.h b/src/path/edge.h index e775cd923..00a5d4d13 100644 --- a/src/path/edge.h +++ b/src/path/edge.h @@ -13,7 +13,6 @@ class Edge { public: Edge() = default; - [[nodiscard]] std::vector points() const; [[nodiscard]] QString label() const; [[nodiscard]] Point start_geometry() const; diff --git a/src/path/face.cpp b/src/path/face.cpp index b95c59fcd..32969beb3 100644 --- a/src/path/face.cpp +++ b/src/path/face.cpp @@ -12,7 +12,7 @@ using namespace omm; bool same_point(const PathPoint* p1, const PathPoint* p2) { - return p1 == p2 || p1->joined_points().contains(p2); + return p1 == p2 || (p1 != nullptr && p1->joined_points().contains(p2)); } bool align_last_edge(const Edge& second_last, Edge& last) @@ -45,6 +45,22 @@ bool align_two_edges(Edge& second_last, Edge& last) } } +template +bool equal_at_offset(const Ts& ts, const Rs& rs, const std::size_t offset) +{ + if (ts.size() != rs.size()) { + return false; + } + + for (std::size_t i = 0; i < ts.size(); ++i) { + const auto j = (i + offset) % ts.size(); + if (!same_point(ts.at(i), rs.at(j))) { + return false; + } + } + return true; +} + } // namespace namespace omm @@ -64,6 +80,15 @@ std::list Face::points() const return points; } +std::deque Face::path_points() const +{ + std::deque points; + for (const auto& edge : edges()) { + points.emplace_back(edge.start_point()); + } + return points; +} + Face::~Face() = default; bool Face::add_edge(const Edge& edge) @@ -111,4 +136,30 @@ QString Face::to_string() const return static_cast(edges).join(", "); } +bool operator==(const Face& a, const Face& b) +{ + const auto points_a = a.path_points(); + const auto points_b = b.path_points(); + if (points_a.size() != points_b.size()) { + return false; + } + const auto points_b_reversed = std::deque(points_b.rbegin(), points_b.rend()); + QStringList pa; + QStringList pb; + for (std::size_t i = 0; i < points_a.size(); ++i) { + pa.append(QString{"%1"}.arg(points_a.at(i)->index())); + pb.append(QString{"%1"}.arg(points_b.at(i)->index())); + } + + for (std::size_t offset = 0; offset < points_a.size(); ++offset) { + if (equal_at_offset(points_a, points_b, offset)) { + return true; + } + if (equal_at_offset(points_a, points_b_reversed, offset)) { + return true; + } + } + return false; +} + } // namespace omm diff --git a/src/path/face.h b/src/path/face.h index 3c77d5024..d276ae9b3 100644 --- a/src/path/face.h +++ b/src/path/face.h @@ -8,6 +8,7 @@ namespace omm { class Point; +class PathPoint; class Edge; class Face @@ -21,10 +22,30 @@ class Face Face& operator=(Face&&) = default; bool add_edge(const Edge& edge); + + /** + * @brief points returns the geometry of each point around the face with proper tangents. + * @note a face with `n` edges yields `n+1` points, because start and end point are listed + * separately. + * that's quite convenient for drawing paths. + * @see path_points + */ [[nodiscard]] std::list points() const; + + /** + * @brief path_points returns the points around the face. + * @note a face with `n` edges yields `n` points, because start and end point are not listed + * separately. + * That's quite convenient for checking face equality. + * @see points + */ + [[nodiscard]] std::deque path_points() const; [[nodiscard]] const std::deque& edges() const; [[nodiscard]] double compute_aabb_area() const; [[nodiscard]] QString to_string() const; + + friend bool operator==(const Face& a, const Face& b); + private: std::deque m_edges; }; diff --git a/src/path/graph.cpp b/src/path/graph.cpp index 0d9274f02..382327995 100644 --- a/src/path/graph.cpp +++ b/src/path/graph.cpp @@ -6,7 +6,8 @@ #include "path/path.h" #include #include -#include +#include +#include namespace { @@ -87,7 +88,6 @@ Graph::Graph(const PathVector& path_vector) last_path_point = point; } } - identify_edges(*m_impl); } std::vector Graph::compute_faces() const @@ -121,6 +121,7 @@ std::vector Graph::compute_faces() const const Impl& m_impl; }; + identify_edges(*m_impl); const auto embedding = m_impl->compute_embedding(); Visitor visitor{*m_impl, faces}; boost::planar_face_traversal(*m_impl, &embedding[0], visitor); @@ -135,7 +136,9 @@ std::vector Graph::compute_faces() const faces.erase(std::next(faces.begin(), largest_face_i)); // NOLINTNEXTLINE(modernize-return-braced-init-list) - return std::vector(faces.begin(), faces.end()); + std::vector vfaces(faces.begin(), faces.end()); + vfaces.erase(std::unique(vfaces.begin(), vfaces.end()), vfaces.end()); + return vfaces; } void Graph::Impl::add_vertex(PathPoint* path_point) @@ -261,4 +264,20 @@ QString Graph::to_dot() const return dot;\ } +void Graph::remove_articulation_edges() const +{ +// auto components = get(boost::edge_index, *m_impl); +// const auto n = boost::biconnected_components(*m_impl, components); +// LINFO << "Found " << n << " biconnected components."; + std::set art_points; + boost::articulation_points(*m_impl, std::inserter(art_points, art_points.end())); + const auto edge_between_articulation_points = [&art_points, this](const Impl::edge_descriptor& e) { + const auto o = [&art_points, this](const Impl::vertex_descriptor& v) { + return degree(v, *m_impl) <= 1 || art_points.contains(v); + }; + return o(e.m_source) && o(e.m_target); + }; + boost::remove_edge_if(edge_between_articulation_points, *m_impl); +} + } // namespace omm diff --git a/src/path/graph.h b/src/path/graph.h index c7470520d..d8aa24f4e 100644 --- a/src/path/graph.h +++ b/src/path/graph.h @@ -27,6 +27,14 @@ class Graph [[nodiscard]] std::vector compute_faces() const; [[nodiscard]] QString to_dot() const; + /** + * @brief remove_articulation_edges an edge is articulated if it connects two articulated points, + * two points of degree one or less or an articulated point with a point of degree one or less. + * Removing articulated edges from a graph increase the number of components by one. + * Articulated edges can never be part of a face. + */ + void remove_articulation_edges() const; + private: std::unique_ptr m_impl; }; diff --git a/src/path/path.cpp b/src/path/path.cpp index ad6cf3052..9060904a2 100644 --- a/src/path/path.cpp +++ b/src/path/path.cpp @@ -228,25 +228,4 @@ void Path::deserialize(AbstractDeserializer& deserializer, const Pointer& root) } } -PathView::PathView(Path& path, std::size_t index, std::size_t size) - : path(&path), index(index), size(size) -{ -} - -bool operator<(const PathView& a, const PathView& b) -{ - static constexpr auto as_tuple = [](const PathView& a) { - return std::tuple{a.path, a.index}; - }; - - // NOLINTNEXTLINE(modernize-use-nullptr) - return as_tuple(a) < as_tuple(b); -} - -std::ostream& operator<<(std::ostream& ostream, const PathView& path_view) -{ - ostream << "Path[" << path_view.path << " " << path_view.index << " " << path_view.size << "]"; - return ostream; -} - } // namespace omm diff --git a/src/path/path.h b/src/path/path.h index 487b1c267..0b8d59a31 100644 --- a/src/path/path.h +++ b/src/path/path.h @@ -16,17 +16,6 @@ class PathPoint; // NOLINTNEXTLINE(bugprone-forward-declaration-namespace) class Path; -struct PathView -{ -public: - explicit PathView(Path& path, std::size_t index, std::size_t size); - friend bool operator<(const PathView& a, const PathView& b); - friend std::ostream& operator<<(std::ostream& ostream, const PathView& path_view); - Path* path; - std::size_t index; - std::size_t size; -}; - // NOLINTNEXTLINE(bugprone-forward-declaration-namespace) class PathVector; diff --git a/src/path/pathpoint.cpp b/src/path/pathpoint.cpp index c614049d8..43fcba168 100644 --- a/src/path/pathpoint.cpp +++ b/src/path/pathpoint.cpp @@ -53,6 +53,22 @@ Point PathPoint::compute_joined_point_geometry(PathPoint& joined) const return geometry; } +bool PathPoint::is_dangling() const +{ + if (path_vector() == nullptr || !path().contains(*this)) { + return true; + } + if (!::contains(path_vector()->paths(), &path())) { + return false; + } + const auto* const path_object = path_vector()->path_object(); + if (path_object == nullptr) { + return false; + } + const auto* const scene = path_object->scene(); + return scene == nullptr || !scene->contains(path_object); +} + QString PathPoint::debug_id() const { auto joins = util::transform(joined_points()); diff --git a/src/path/pathpoint.h b/src/path/pathpoint.h index cd99c281a..702b35843 100644 --- a/src/path/pathpoint.h +++ b/src/path/pathpoint.h @@ -42,6 +42,7 @@ class PathPoint void disjoin(); [[nodiscard]] PathVector* path_vector() const; [[nodiscard]] Point compute_joined_point_geometry(PathPoint& joined) const; + [[nodiscard]] bool is_dangling() const; /** * @brief debug_id returns an string to identify the point uniquely at this point in time diff --git a/src/path/pathvector.cpp b/src/path/pathvector.cpp index bf75c5cae..9eceb1f4d 100644 --- a/src/path/pathvector.cpp +++ b/src/path/pathvector.cpp @@ -163,12 +163,30 @@ QPainterPath PathVector::outline() const std::vector PathVector::faces() const { Graph graph{*this}; - auto faces = graph.compute_faces(); + graph.remove_articulation_edges(); + const auto faces = graph.compute_faces(); std::vector qpps; qpps.reserve(faces.size()); for (const auto& face : faces) { qpps.emplace_back(Path::to_painter_path(face.points())); } + + for (bool path_changed = true; path_changed;) + { + path_changed = false; + for (auto& q1 : qpps) { + for (auto& q2 : qpps) { + if (&q1 == &q2) { + continue; + } + if (q1.contains(q2)) { + path_changed = true; + q1 -= q2; + } + } + } + } + return qpps; } diff --git a/src/path/pathview.cpp b/src/path/pathview.cpp new file mode 100644 index 000000000..6722916f4 --- /dev/null +++ b/src/path/pathview.cpp @@ -0,0 +1,37 @@ +#include "path/pathview.h" +#include +#include +#include "path/path.h" + +namespace omm +{ + +PathView::PathView(Path& path, std::size_t index, std::size_t size) + : path(&path), index(index), size(size) +{ +} + +std::deque PathView::points() const +{ + auto points = path->points(); + points.erase(points.begin(), std::next(points.begin(), index)); + points.erase(std::next(points.begin(), size), points.end()); + return points; +} + +bool operator<(const PathView& a, const PathView& b) +{ + static constexpr auto as_tuple = [](const PathView& a) { + return std::tuple{a.path, a.index}; + }; + // NOLINTNEXTLINE(modernize-use-nullptr) + return as_tuple(a) < as_tuple(b); +} + +std::ostream& operator<<(std::ostream& ostream, const PathView& path_view) +{ + ostream << "Path[" << path_view.path << " " << path_view.index << " " << path_view.size << "]"; + return ostream; +} + +} // namespace omm diff --git a/src/path/pathview.h b/src/path/pathview.h new file mode 100644 index 000000000..684e91f5f --- /dev/null +++ b/src/path/pathview.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +namespace omm +{ + +class Path; +class PathPoint; + +struct PathView +{ +public: + explicit PathView(Path& path, std::size_t index, std::size_t size); + friend bool operator<(const PathView& a, const PathView& b); + friend std::ostream& operator<<(std::ostream& ostream, const PathView& path_view); + [[nodiscard]] std::deque points() const; + Path* path; + std::size_t index; + std::size_t size; +}; + +} // namepsace diff --git a/src/renderers/offscreenrenderer.cpp b/src/renderers/offscreenrenderer.cpp index 89b69f89e..f0b97dee5 100644 --- a/src/renderers/offscreenrenderer.cpp +++ b/src/renderers/offscreenrenderer.cpp @@ -42,6 +42,16 @@ const std::vector OffscreenRenderer::fragment_sh QT_TRANSLATE_NOOP("OffscreenRenderer", "view_pos"), ShaderInput::Kind::Varying, }, + { + Type::Integer, + QT_TRANSLATE_NOOP("OffscreenRenderer", "object_id"), + ShaderInput::Kind::Uniform, + }, + { + Type::Integer, + QT_TRANSLATE_NOOP("OffscreenRenderer", "path_id"), + ShaderInput::Kind::Uniform, + }, }; QString OffscreenRenderer::ShaderInput::tr_name() const @@ -70,6 +80,8 @@ uniform mat3 view_transform; uniform vec2 view_size; uniform vec2 roi_tl; uniform vec2 roi_br; +uniform int object_id; +uniform int path_id; vec2 unc(vec2 centered) { return (centered + vec2(1.0, 1.0)) / 2.0; @@ -311,6 +323,8 @@ Texture OffscreenRenderer::render(const Object& object, ::set_uniform(*this, "view_size", Vec2f(options.device.width(), options.device.height())); ::set_uniform(*this, "roi_tl", Vec2f(roi.topLeft())); ::set_uniform(*this, "roi_br", Vec2f(roi.bottomRight())); + ::set_uniform(*this, "object_id", options.object_id); + ::set_uniform(*this, "path_id", options.path_id); m_vertices.bind(); diff --git a/src/renderers/painter.cpp b/src/renderers/painter.cpp index d819fec5d..d09a78717 100644 --- a/src/renderers/painter.cpp +++ b/src/renderers/painter.cpp @@ -105,7 +105,9 @@ void Painter::toast(const Vec2f& pos, const QString& text) const } QBrush -Painter::make_brush(const Style& style, const Object& object, const PainterOptions& options) +Painter::make_brush(const Style& style, + const Object& object, + const PainterOptions& options) { if (style.property(omm::Style::BRUSH_IS_ACTIVE_KEY)->value()) { if (style.property("gl-brush")->value()) { diff --git a/src/renderers/painter.h b/src/renderers/painter.h index d08f42142..7f4ed6061 100644 --- a/src/renderers/painter.h +++ b/src/renderers/painter.h @@ -43,7 +43,9 @@ class Painter void toast(const Vec2f& pos, const QString& text) const; - static QBrush make_brush(const Style& style, const Object& object, const PainterOptions& options); + static QBrush make_brush(const Style& style, + const Object& object, + const PainterOptions& options); static QPen make_pen(const Style& style, const Object& object); static QBrush make_simple_brush(const Style& style); diff --git a/src/renderers/painteroptions.h b/src/renderers/painteroptions.h index 183fa9650..dc603317e 100644 --- a/src/renderers/painteroptions.h +++ b/src/renderers/painteroptions.h @@ -18,6 +18,8 @@ struct PainterOptions const Style* default_style = nullptr; const bool device_is_viewport; const QPaintDevice& device; + mutable std::size_t object_id; + mutable std::size_t path_id = 0; }; } // namespace omm diff --git a/src/scene/disjointpathpointsetforest.cpp b/src/scene/disjointpathpointsetforest.cpp index 37f496f6d..abd66c2bc 100644 --- a/src/scene/disjointpathpointsetforest.cpp +++ b/src/scene/disjointpathpointsetforest.cpp @@ -85,25 +85,16 @@ void DisjointPathPointSetForest::serialize(AbstractSerializer& serializer, const } void DisjointPathPointSetForest::remove_dangling_points() +{ + remove_if(std::mem_fn(&PathPoint::is_dangling)); +} + +void DisjointPathPointSetForest::remove_if(const std::function& predicate) { for (auto& set : m_forest) { - std::erase_if(set, [](const PathPoint* point) { - static constexpr auto is_part_of_scene = [](const PathPoint* point) { - const auto* const path_object = point->path_vector()->path_object(); - return path_object->scene() != nullptr && path_object->scene()->contains(path_object); - }; - return point == nullptr - || point->path_vector() == nullptr - || !point->path().contains(*point) - || !::contains(point->path_vector()->paths(), &point->path()) - || point->path_vector()->path_object() == nullptr - || !is_part_of_scene(point); - }); + std::erase_if(set, predicate); } - - m_forest.erase(std::remove_if(m_forest.begin(), m_forest.end(), [](const auto& set) { - return set.empty(); - }), m_forest.end()); + remove_empty_sets(); } void DisjointPathPointSetForest::replace(const std::map& dict) @@ -117,10 +108,7 @@ void DisjointPathPointSetForest::replace(const std::map& } old_set = new_set; } - - m_forest.erase(remove_if(m_forest.begin(), m_forest.end(), [](const auto& set) { - return set.empty(); - }), m_forest.end()); + remove_empty_sets(); } void DisjointPathPointSetForest::serialize_impl(AbstractSerializer& serializer, const Pointer& root) const diff --git a/src/scene/disjointpathpointsetforest.h b/src/scene/disjointpathpointsetforest.h index c0ed1cc4e..f558756a6 100644 --- a/src/scene/disjointpathpointsetforest.h +++ b/src/scene/disjointpathpointsetforest.h @@ -16,6 +16,7 @@ class DisjointPathPointSetForest : public DisjointSetForest, public void deserialize(AbstractDeserializer& deserializer, const Pointer& root) override; void serialize(AbstractSerializer& serializer, const Pointer& root) const override; void remove_dangling_points(); + void remove_if(const std::function& predicate); void replace(const std::map& dict); private: diff --git a/src/tools/pathtool.cpp b/src/tools/pathtool.cpp index c19f673c1..5e298d57d 100644 --- a/src/tools/pathtool.cpp +++ b/src/tools/pathtool.cpp @@ -119,7 +119,7 @@ class LeftButtonPressImpl void ensure_active_path(Scene& scene, PathTool::Current& current) { - if (current.path == nullptr) { + if (current.path_object == nullptr) { start_macro(); static constexpr auto insert_mode = Application::InsertionMode::Default; auto& path_object = Application::instance().insert_object(PathObject::TYPE, insert_mode); diff --git a/test/unit/pathtest.cpp b/test/unit/pathtest.cpp index bc9628529..220ac9583 100644 --- a/test/unit/pathtest.cpp +++ b/test/unit/pathtest.cpp @@ -4,15 +4,45 @@ #include "path/graph.h" #include "path/edge.h" #include "path/face.h" +#include "path/pathpoint.h" #include "scene/disjointpathpointsetforest.h" -using namespace omm; +namespace +{ -auto make_face(const PathVector& pv, const std::vector>& indices) +class EdgeLoop { - Face face; +public: + explicit EdgeLoop(const std::size_t n) + : m_path(m_path_vector.add_path(std::make_unique())) + , m_edges(create_edge_loop(n)) + { + } + + const auto& edges() const { return m_edges; } + +private: + omm::PathVector m_path_vector; + omm::Path& m_path; + const std::deque m_edges; + + std::deque create_edge_loop(const std::size_t n) const + { + assert(n >= 2); + std::deque edges(n); + for (std::size_t i = 0; i < n; ++i) { + edges.at(i).a = &m_path.add_point({}); + edges.at((i + 1) % n).b = edges.at(i).a; + } + return edges; + } +}; + +auto make_face(const omm::PathVector& pv, const std::vector>& indices) +{ + omm::Face face; for (const auto& [ai, bi] : indices) { - Edge edge; + omm::Edge edge; edge.a = &pv.point_at_index(ai); edge.b = &pv.point_at_index(bi); face.add_edge(edge); @@ -20,46 +50,88 @@ auto make_face(const PathVector& pv, const std::vector>& ind return face; } -// Considering the loop without joints (A) --e1-- (B) --e2-- (A), e1 and e2 will be considered equal. -// However, this case does not occur during testing. -bool edge_equal(const Edge& a, const Edge& b) +omm::Face create_face(const std::deque& edges, const int offset, const bool reverse) { - return std::set{a.a, a.b} == std::set{b.a, b.b}; + std::deque es; + std::rotate_copy(edges.begin(), edges.begin() + offset, edges.end(), std::back_insert_iterator(es)); + if (reverse) { + std::reverse(es.begin(), es.end()); + } + omm::Face face; + for (const auto& edge : es) { + static constexpr auto r = [](const omm::Edge& e) { + omm::Edge r; + r.a = e.b; + r.b = e.a; + return r; + }; + face.add_edge(reverse ? r(edge) : edge); + } + return face; } -bool operator==(const Face& a, const Face& b) +} // namespace + +TEST(Path, FaceAddEdge) { - const auto& a_edges = a.edges(); - const auto& b_edges = b.edges(); - if (a_edges.size() != b_edges.size()) { - return false; - } + const EdgeLoop loop(4); + + static constexpr auto expect_true_perms = { + std::array{0, 1, 2, 3}, + std::array{1, 2, 3, 0}, + std::array{3, 2, 1, 0}, + std::array{2, 1, 0, 3}, + }; - const auto n = a_edges.size(); - const auto rotation_match = [n, &a_edges, &b_edges](const int offset, bool reverse) { - for (std::size_t j = 0; j < n; ++j) { - const auto ai = reverse ? n - j - 1 : j; - const auto bi = (j + offset) % n; - if (!edge_equal(a_edges[ai], b_edges[bi])) { - return false; - } + for (const auto permutation : expect_true_perms) { + omm::Face face; + for (const std::size_t i : permutation) { + EXPECT_TRUE(face.add_edge(loop.edges().at(i))); } - return true; + } + + // It adding the third edge is expected to fail because there's no way to orient it such that it + // has a common point with the previous edge. + // Hence, adding a fourth edge is not required. + static constexpr auto expect_false_perms = { + std::array{0, 1, 3}, + std::array{2, 1, 3}, + std::array{1, 2, 0}, + std::array{1, 0, 2}, }; - for (std::size_t i = 0; i < n; ++i) { - if (rotation_match(i, false) || rotation_match(i, true)) { - return true; + for (const auto permutation : expect_false_perms) { + omm::Face face; + for (std::size_t k = 0; k < permutation.size() - 1; ++k) { + EXPECT_TRUE(face.add_edge(loop.edges().at(permutation.at(k)))); } + EXPECT_FALSE(face.add_edge(loop.edges().at(permutation.back()))); + } +} + +TEST(Path, FaceEquality) +{ + EdgeLoop loop(4); + for (std::size_t i = 0; i < loop.edges().size(); ++i) { + EXPECT_EQ(create_face(loop.edges(), 0, false), create_face(loop.edges(), i, false)); + EXPECT_EQ(create_face(loop.edges(), 0, true), create_face(loop.edges(), i, false)); + EXPECT_EQ(create_face(loop.edges(), i, true), create_face(loop.edges(), 0, true)); + } + + auto scrambled_edges = loop.edges(); + std::swap(scrambled_edges.at(0), scrambled_edges.at(1)); + for (std::size_t i = 0; i < loop.edges().size(); ++i) { + EXPECT_NE(create_face(scrambled_edges, 0, false), create_face(loop.edges(), i, false)); + EXPECT_NE(create_face(scrambled_edges, 0, true), create_face(loop.edges(), i, false)); + EXPECT_NE(create_face(scrambled_edges, i, true), create_face(loop.edges(), 0, true)); } - return false; } TEST(Path, face_detection) { - PathVector path_vector; + omm::PathVector path_vector; // define following path vector: // @@ -68,6 +140,10 @@ TEST(Path, face_detection) // | | | // (0,4) --- (1,5) --- (6) + using omm::Path; + using omm::Point; + using omm::Graph; + const auto as = path_vector.add_path(std::make_unique(std::deque{ Point{{0.0, 0.0}}, // 0 Point{{1.0, 0.0}}, // 1 @@ -88,11 +164,8 @@ TEST(Path, face_detection) path_vector.joined_points().insert({as[2], bs[3]}); const Graph graph{path_vector}; - const auto dot = graph.to_dot(); - LINFO << dot; - const auto faces = graph.compute_faces(); ASSERT_EQ(faces.size(), 2); - ASSERT_TRUE(faces[0] == make_face(path_vector, {{0, 1}, {1, 2}, {2, 3}, {3, 4}})); - ASSERT_TRUE(faces[1] == make_face(path_vector, {{5, 6}, {6, 7}, {7, 8}, {1, 2}})); + ASSERT_EQ(faces[0], make_face(path_vector, {{0, 1}, {1, 2}, {2, 3}, {3, 4}})); + ASSERT_EQ(faces[1], make_face(path_vector, {{5, 6}, {6, 7}, {7, 8}, {1, 2}})); }