/** @file PlanarGraph – a graph geometrically embedded in the plane. */ /* * Authors: * Rafał Siejakowski * * Copyright 2022 the Authors * * This library is free software; you can redistribute it and/or * modify it either under the terms of the GNU Lesser General Public * License version 2.1 as published by the Free Software Foundation * (the "LGPL") or, at your option, under the terms of the Mozilla * Public License Version 1.1 (the "MPL"). If you do not alter this * notice, a recipient may use your version of this file under either * the MPL or the LGPL. * * You should have received a copy of the LGPL along with this library * in the file COPYING-LGPL-2.1; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * You should have received a copy of the MPL along with this library * in the file COPYING-MPL-1.1 * * The contents of this file are subject to the Mozilla Public License * Version 1.1 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY * OF ANY KIND, either express or implied. See the LGPL or the MPL for * the specific language governing rights and limitations. */ // WARNING: This is a private header. Do not include it directly. #ifndef LIB2GEOM_SEEN_PLANAR_GRAPH_H #define LIB2GEOM_SEEN_PLANAR_GRAPH_H #include #include #include #include <2geom/angle.h> #include <2geom/coord.h> #include <2geom/line.h> #include <2geom/point.h> #include <2geom/path.h> #include <2geom/path-intersection.h> #include <2geom/utils.h> namespace Geom { /** * \class PlanarGraph * \brief Planar graph - a graph geometrically embedded in the plane. * * A planar graph is composed of vertices with assigned locations (as points in the plane) * and of edges (arcs), which are imagined as non-intersecting paths in the plane connecting * the vertices. The edges can hold user-supplied labels (e.g., weights) which support event * callbacks for when the graph is reconfigured, allowing the labels to be updated accordingly. * * \tparam EdgeLabel A user-supplied type; an object of this type will be attached to each * edge of the planar graph (e.g., a "weight" of the edge). The type must * satisfy requirements described further below. * * In order to construct a planar graph, you should specify the clumping precision (passed as * a constructor argument) and then use the method insertEdge() to add edges to the graph, as * many times as necessary. The graph will automatically figure out the locations of the * vertices based on the endpoints of the inserted edges. Vertices will be combined into one * when they are positioned within the distance specified as the clumping threshold, and the * inserted edges will be attached to them accordingly. It is also possible to insert paths * (typically, closed) not attached to any vertices, using the method insertDetached(). * * After the edges are inserted, the graph is in a potentially degenerate state, where several * edges may exactly coincide in part or in full. If this is not desired, you can regularize * the graph by calling regularize(). During the regularization process, any overlapping edges * are combined into one. Partially overlapping edges are first split into overlapping and * non-overlapping portions, after which the overlapping portions are combined. If the edges * or their parts overlap but run in opposite directions, one of them will be reversed before * being merged with the other one. The overlaps are detected using the precision setting passed * as the clumping precision in the constructor argument. * * Note however that the regularization procedure does NOT detect transverse intersections * between the edge paths: if such intersections are not desired, the user must pass non-\ * intersecting paths to the insertEdge() method (the paths may still have common endpoints, * which is fine: that's how common vertices are created). * * The insertion of new edges invalidates the regularized status, which you can check at any * time by calling isRegularized(). * * The vertices stored by the graph are sorted by increasing X-coordinate, and if they have * equal X-coordinates, by increasing Y-coordinate. Even before regularization, incidences of * edges to each vertex are sorted by increasing azimuth of the outgoing tangent (departure * heading, but in radians, in the interval \f$(-\pi, \pi]\f$). After regularization, the edges * around each vertex are guaranteed to be sorted counterclockwise (when the Y-axis points up) * by where they end up going eventually, even if they're tangent at the vertex and therefore * have equal or nearly equal departure azimuths. * * \note * Requirements on the \c EdgeLabel template parameter type. * In order for the template to instantiate correctly, the following must be satisfied: * \li The \c EdgeLabel type provides a method \c onReverse() which gets called whenever * the orientation of the labeled edge is reversed. This is useful when implementing * a directed graph, since the label can keep track of the logical direction. * \li The \c EdgeLabel type provides a method \c onMergeWith(EdgeLabel const&) which gets * called when the labeled edge is combined with a geometrically identical (coinciding) * edge (both combined edges having the same orientations). The label of the edge merged * with the current one is provided as an argument to the method. This is useful when * implementing a graph with weights: for example, when two edges are merged, you may * want to combine their weights in some way. * \li There is a method \c onDetach() called when the edge is removed from the graph. The * edge objects are never destroyed but may be disconnected from the graph when they're no * longer needed; this allows the user to put the labels of such edges in a "dead" state. * \li The \c EdgeLabel objects must be copy-constructible and copy-assignable. This is * because when an edge is subdivided into two, the new edges replacing it get decorated * with copies of the original edge's label. */ template #if __cplusplus >= 202002L requires requires(EdgeLabel el, EdgeLabel const &other) { el.onReverse(); el.onMergeWith(other); el.onDetach(); el = other; } #endif class PlanarGraph { public: /** Represents the joint between an edge and a vertex. */ struct Incidence { using Sign = bool; inline static Sign const START = false; inline static Sign const END = true; double azimuth; ///< Angle of the edge's departure. unsigned index; ///< Index of the edge in the parent graph. Sign sign; ///< Whether this is the start or end of the edge. bool invalid = false; ///< Whether this incidence has been marked for deletion. Incidence(unsigned edge_index, double departure_azimuth, Sign which_end) : azimuth{departure_azimuth} , index{edge_index} , sign{which_end} { } ~Incidence() = default; /// Compare incidences based on their azimuths in radians. inline bool operator<(Incidence const &other) const { return azimuth < other.azimuth; } /// Compare the azimuth of an incidence with the given angle. inline bool operator<(double angle) const { return azimuth < angle; } /// Check equality (only tests edges and their ends). inline bool operator==(Incidence const &other) const { return index == other.index && sign == other.sign; } }; using IncIt = typename std::list::iterator; using IncConstIt = typename std::list::const_iterator; /** Represents the vertex of a planar graph. */ class Vertex { private: Point const _position; ///< Geometric position of the vertex. std::list _incidences; ///< List of incidences of edges to this vertex. unsigned mutable _flags = 0; ///< User-settable flags. inline static Point::LexLess const _cmp; ///< Point sorting function object. public: Vertex(Point const &pos) : _position{pos} { } /** Get the geometric position of the vertex. */ Point const &point() const { return _position; } /** Get the list of incidences to the vertex. */ auto const &getIncidences() const { return _incidences; } /** Compare vertices based on their coordinates (lexicographically). */ bool operator<(Vertex const &other) const { return _cmp(_position, other._position); } unsigned flags() const { return _flags; } ///< Get the user flags. void setFlags(unsigned flags) const { _flags = flags; } ///< Set the user flags. /** Get the cyclic-next incidence after the passed one, in the CCW direction. */ IncConstIt cyclicNextIncidence(IncConstIt it) const { return cyclic_next(it, _incidences); } /** Get the cyclic-next incidence after the passed one, in the CW direction. */ IncConstIt cyclicPrevIncidence(IncConstIt it) const { return cyclic_prior(it, _incidences); } /** The same but with pointers. */ Incidence *cyclicNextIncidence(Incidence *from) { return &(*cyclicNextIncidence(_incidencePtr2It(from))); } Incidence *cyclicPrevIncidence(Incidence *from) { return &(*cyclicPrevIncidence(_incidencePtr2It(from))); } private: /** Same as above, but not const (so only for private use). */ IncIt cyclicNextIncidence(IncIt it) { return cyclic_next(it, _incidences); } IncIt cyclicPrevIncidence(IncIt it) { return cyclic_prior(it, _incidences); } /** Insert an incidence; for internal use by the PlanarGraph class. */ Incidence &_addIncidence(unsigned edge_index, double azimuth, typename Incidence::Sign sign) { auto where = std::find_if(_incidences.begin(), _incidences.end(), [=](auto &inc) -> bool { return inc.azimuth >= azimuth; }); return *(_incidences.emplace(where, edge_index, azimuth, sign)); } /** Return a valid iterator to an incidence passed by pointer; * if the pointer is invalid, return a start iterator. */ IncIt _incidencePtr2It(Incidence *pointer) { auto it = std::find_if(_incidences.begin(), _incidences.end(), [=](Incidence const &i) -> bool { return &i == pointer; }); return (it == _incidences.end()) ? _incidences.begin() : it; } friend class PlanarGraph; }; using VertexIterator = typename std::list::iterator; /** Represents an edge of the planar graph. */ struct Edge { Vertex *start = nullptr, *end = nullptr; ///< Start and end vertices. Path path; ///< The path associated to the edge. bool detached = false; ///< Whether the edge is detached from the graph. bool inserted_as_detached = false; ///< Whether the edge was inserted as detached. EdgeLabel mutable label; ///< The user-supplied label of the edge. /** Construct an edge with a given label. */ Edge(Path &&movein_path, EdgeLabel &&movein_label) : path{movein_path} , label{movein_label} { } /** Detach the edge from the graph. */ void detach() { detached = true; label.onDetach(); } }; using EdgeIterator = typename std::vector::iterator; using EdgeConstIterator = typename std::vector::const_iterator; private: double const _precision; ///< Numerical epsilon for vertex clumping. std::list _vertices; ///< Vertices of the graph. std::vector _edges; ///< Edges of the graph. std::vector< std::pair > _junk; ///< Incidences that should be purged. bool _regularized = true; // An empty graph is (trivially) regularized. public: PlanarGraph(Coord precision = EPSILON) : _precision{precision} { } std::list const &getVertices() const { return _vertices; } std::vector const &getEdges() const { return _edges; } Edge const &getEdge(size_t index) const { return _edges.at(index); } size_t getEdgeIndex(Edge const &edge) const { return &edge - _edges.data(); } double getPrecision() const { return _precision; } size_t numVertices() const { return _vertices.size(); } size_t numEdges(bool include_detached = true) const { if (include_detached) { return _edges.size(); } return std::count_if(_edges.begin(), _edges.end(), [](Edge const &e) -> bool { return !e.detached; }); } /** Check if the graph has been regularized. */ bool isRegularized() const { return _regularized; } // 0x1p-50 is about twice the distance between M_PI and the next representable double. void regularize(double angle_precision = 0x1p-50, bool remove_collapsed_loops = true); /** Allocate memory to store the specified number of edges. */ void reserveEdgeCapacity(size_t capacity) { _edges.reserve(capacity); } unsigned insertEdge(Path &&path, EdgeLabel &&edge = EdgeLabel()); unsigned insertDetached(Path &&path, EdgeLabel &&edge = EdgeLabel()); /** Edge insertion with a copy of the path. */ unsigned insertEdge(Path const &path, EdgeLabel &&edge = EdgeLabel()) { return insertEdge(Path(path), std::forward(edge)); } unsigned insertDetached(Path const &path, EdgeLabel &&edge = EdgeLabel()) { return insertDetached(Path(path), std::forward(edge)); } /** \brief Find the incidence at the specified endpoint of the edge. * * \param edge_index The index of the edge whose incidence we wish to query. * \param sign Which end of the edge do we want an incidence of. * \return A pair consisting of pointers to the vertex and the incidence. * If not found, both pointers will be null. */ std::pair getIncidence(unsigned edge_index, typename Incidence::Sign sign) const { if (edge_index >= _edges.size() || _edges[edge_index].detached) { return {nullptr, nullptr}; } Vertex *vertex = (sign == Incidence::START) ? _edges[edge_index].start : _edges[edge_index].end; if (!vertex) { return {nullptr, nullptr}; } auto it = std::find(vertex->_incidences.begin(), vertex->_incidences.end(), Incidence(edge_index, 42, sign)); // azimuth doesn't matter. if (it == vertex->_incidences.end()) { return {nullptr, nullptr}; } return {vertex, &(*it)}; } /** * \brief Go clockwise or counterclockwise around the vertex and find the next incidence. * The notions of "clockwise"/"counterclockwise" correspond to the y-axis pointing up. * * \param vertex The vertex around which to orbit. * \param incidence The incidence from which to start traversal. * \param clockwise Whether to go clockwise instead of (default) counterclockwise. * \return The next incidence encountered going in the specified direction. */ inline Incidence const &nextIncidence(VertexIterator const &vertex, IncConstIt const &incidence, bool clockwise = false) const { return clockwise ? *(vertex->_cyclicPrevIncidence(incidence)) : *(vertex->_cyclicNextIncidence(incidence)); } /** As above, but taking references instead of iterators. */ inline Incidence const &nextIncidence(Vertex const &vertex, Incidence const &incidence, bool clockwise = false) const { IncConstIt it = std::find(vertex._incidences.begin(), vertex._incidences.end(), incidence); if (it == vertex._incidences.end()) { return incidence; } return clockwise ? *(vertex.cyclicPrevIncidence(it)) : *(vertex.cyclicNextIncidence(it)); } /** As above, but return an iterator to a const incidence. */ inline IncConstIt nextIncidenceIt(Vertex const &vertex, Incidence const &incidence, bool clockwise = false) const { IncConstIt it = std::find(vertex._incidences.begin(), vertex._incidences.end(), incidence); if (it == vertex._incidences.end()) { return vertex._incidences.begin(); } return clockwise ? vertex.cyclicPrevIncidence(it) : vertex.cyclicNextIncidence(it); } inline IncConstIt nextIncidenceIt(Vertex const &vertex, IncConstIt const &incidence, bool clockwise = false) const { return clockwise ? vertex.cyclicPrevIncidence(incidence) : vertex.cyclicNextIncidence(incidence); } /** As above, but start at the prescribed departure azimuth (in radians). * * \return A pointer to the incidence emanating from the vertex at or immediately after * the specified azimuth, when going around the vertex in the specified direction. * If the vertex has no incidences, return value is nullptr. */ Incidence *nextIncidence(VertexIterator const &vertex, double azimuth, bool clockwise = false) const; /** Get the incident path, always oriented away from the vertex. */ Path getOutgoingPath(Incidence const *incidence) const { return incidence ? _getPathImpl(incidence, Incidence::START) : Path(); } /** Get the incident path, always oriented towards the vertex. */ Path getIncomingPath(Incidence const *incidence) const { return incidence ? _getPathImpl(incidence, Incidence::END) : Path(); } private: inline Path _getPathImpl(Incidence const *incidence, typename Incidence::Sign origin) const { return (incidence->sign == origin) ? _edges[incidence->index].path : _edges[incidence->index].path.reversed(); } /** Earmark an incidence for future deletion. */ inline void _throwAway(Vertex *vertex, Incidence *incidence) { if (!vertex || !incidence) { return; } incidence->invalid = true; _junk.emplace_back(vertex, incidence); } // Topological reconfiguration functions; see their definitions for documentation. bool _compareAndReglue(Vertex &vertex, Incidence *first, Incidence *second, bool deloop); Vertex *_ensureVertexAt(Point const &position); void _mergeCoincidingEdges(Incidence *first, Incidence *second); void _mergeShorterLonger(Vertex &vertex, Incidence *shorter, Incidence *longer, PathTime const &time_on_longer); void _mergeWyeConfiguration(Vertex &vertex, Incidence *first, Incidence *second, PathIntersection const &split); void _purgeJunkIncidences(); void _reglueLasso(Vertex &vertex, Incidence *first, Incidence *second, PathIntersection const &split); bool _reglueTeardrop(Vertex &vertex, Incidence *first, Incidence *second, bool deloop); void _reglueTangentFan(Vertex &vertex, IncIt const &first, IncIt const &last, bool deloop); void _regularizeVertex(Vertex &vertex, double angle_precision, bool deloop); // === Static stuff === /** Return the angle between the vector and the positive X axis or 0 if undefined. */ inline static double _getAzimuth(Point const &vec) { return vec.isZero() ? 0.0 : atan2(vec); } /** Return path time corresponding to the same point on the reversed path. */ inline static PathTime _reversePathTime(PathTime const &time, Path const &path) { int new_index = path.size() - time.curve_index - 1; Coord new_time = 1.0 - time.t; if (new_index < 0) { new_index = 0; new_time = 0; } return PathTime(new_index, new_time); } /** Return path time at the end of the path. */ inline static PathTime _pathEnd(Path const &path) { return PathTime(path.size() - 1, 1.0); } inline static auto const PATH_START = PathTime(0, 0); public: static double closedPathArea(Path const &path); static bool deviatesLeft(Path const &first, Path const &second); }; /** * \brief Insert a new vertex or reuse an existing one. * * Ensures that there is a vertex at or near the specified position * (within the distance of _precision). * * \param pos The desired geometric position of the new vertex. * \return A pointer to the inserted vertex or a pre-existing vertex near the * desired position. */ template typename PlanarGraph::Vertex *PlanarGraph::_ensureVertexAt(Point const &pos) { auto const insert_at_front = [&, this]() -> Vertex* { _vertices.emplace_front(pos); return &(_vertices.front()); }; if (_vertices.empty()) { return insert_at_front(); } // TODO: Use a heap? auto it = std::find_if(_vertices.begin(), _vertices.end(), [&](Vertex const &v) -> bool { return Vertex::_cmp(pos, v._position); // existing vertex position > pos. }); if (it != _vertices.end()) { if (are_near(pos, it->_position, _precision)) { return &(*it); // Reuse existing vertex. } if (it == _vertices.begin()) { return insert_at_front(); } } // Look at the previous element, reuse if near, insert before `it` otherwise. return &(*(are_near(pos, std::prev(it)->_position, _precision) ? std::prev(it) : _vertices.emplace(it, pos))); } /** * \brief Move-insert a new labeled edge into the planar graph. * * \param path The geometric path of the edge. * \param label Optionally, the label (extra user data) associated to this edge. * If absent, a default-constructed label will be used. * \return The index of the inserted edge. */ template unsigned PlanarGraph::insertEdge(Path &&path, EdgeLabel &&label) { unsigned edge_index = _edges.size(); auto &inserted = _edges.emplace_back(std::forward(path), std::forward(label)); // Calculate the outgoing azimuths at both endpoints. double const start_azimuth = _getAzimuth(inserted.path.initialUnitTangent()); double const end_azimuth = _getAzimuth(-inserted.path.finalUnitTangent()); // Get the endpoints into the graph. auto start = _ensureVertexAt(inserted.path.initialPoint()); auto end = _ensureVertexAt(inserted.path.finalPoint()); // Inform the edge about its endpoints. inserted.start = start; inserted.end = end; // Add incidences at the endpoints. start->_addIncidence(edge_index, start_azimuth, Incidence::START); end->_addIncidence(edge_index, end_azimuth, Incidence::END); _regularized = false; return edge_index; } /** * \brief Move-insert a new labeled edge but do not connect it to the graph. * * Although the graph will hold the path data of an edge inserted in this way, the edge * will not be connected to any vertex. This can be used to store information about closed * paths (loops) in the instance, without having to specify starting points for them. * * \param path The geometric path of the edge. * \param label Optionally, the label (extra user data) associated to this edge; if absent, * the label will be default-constructed. * \return The index of the inserted edge. */ template unsigned PlanarGraph::insertDetached(Path &&path, EdgeLabel &&label) { unsigned edge_index = _edges.size(); auto &inserted = _edges.emplace_back(std::forward(path), std::forward(label)); inserted.detached = true; inserted.inserted_as_detached = true; return edge_index; } /** Remove incidences previously marked as junk. */ template void PlanarGraph::_purgeJunkIncidences() { for (auto &[vertex, incidence] : _junk) { Incidence *to_remove = incidence; auto it = std::find_if(vertex->_incidences.begin(), vertex->_incidences.end(), [=](Incidence const &inc) -> bool { return &inc == to_remove; }); if (it != vertex->_incidences.end()) { vertex->_incidences.erase(it); } } _junk.clear(); } /** * \brief Merge overlapping edges or their portions, adding vertices if necessary. * * \param angle_precision The numerical epsilon for radian angle comparisons. * \param remove_collapsed_loops Whether to detach edges with both ends incident to the same * vertex (loops) when these loops don't enclose any area. * * This function performs the following operations: * \li Edges that are tangent at a vertex but don't otherwise overlap are sorted correctly * in the counterclockwise cyclic order around the vertex. * \li Degenerate loops which don't enclose any area are removed if the argument is true. * \li Edges that coincide completely are reversed if needed and merged into one. * \li Edges that coincide partially are split into overlapping and non-overlapping portions. * Any overlapping portions are oriented consistently and then merged. * \li As a sub-case of the above, any non-degenerate loop with an initial self-everlap * (a "lasso") is replaced with a shorter non-overlapping loop and a simple path leading * to it. */ template void PlanarGraph::regularize(double angle_precision, bool remove_collapsed_loops) { for (auto it = _vertices.begin(); it != _vertices.end(); ++it) { // Note: the list of vertices may grow during the execution of this loop, // so don't replace it with a range-for (which stores the end iterator). // We want the loop to carry on going over the elements it inserted. if (it->_incidences.size() < 2) { continue; } _regularizeVertex(*it, angle_precision, remove_collapsed_loops); } _purgeJunkIncidences(); _regularized = true; } /** * \brief Analyze and regularize all edges emanating from a given vertex. * * This function goes through the list of incidences at the vertex (roughly sorted by * azimuth, i.e., departure heading in radians), picking out runs of mutually tangent * edges and calling _reglueTangentFan() on each run. The algorithm is quite complicated * because the incidences have to be treated as a cyclic container and a run of mutually * tangent edges may straddle the "end" of the list, including the possibility that the * entire list is a single such run. * * \param vertex The vertex whose incidences should be analyzed. * \param angle_precision The numerical epsilon for radian angle comparisons. * \param deloop Whether loops that don't enclose any area should be detached. */ template void PlanarGraph::_regularizeVertex(typename PlanarGraph::Vertex &vertex, double angle_precision, bool deloop) { auto &incidences = vertex._incidences; /// Compare two polar angles in the interval [-π, π] modulo 2π to within angle_precision: auto const angles_equal = [=](double az1, double az2) -> bool { static double const twopi = 2.0 * M_PI; return are_near(std::fmod(az1 + twopi, twopi), std::fmod(az2 + twopi, twopi), angle_precision); }; IncIt run_begin; // First element in the last discovered run of equal azimuths. /// Find and reglue runs of nearly identical azimuths in the specified range. auto const process_runs = [&](IncIt begin, IncIt end) -> bool { double current_azimuth = 42; // Invalid radian angle. bool in_a_run = false; for (auto it = begin; it != end; ++it) { bool const equal = angles_equal(it->azimuth, current_azimuth); if (equal && !in_a_run) { run_begin = std::prev(it); // Save to enclosing scope. in_a_run = true; } else if (!equal && in_a_run) { _reglueTangentFan(vertex, run_begin, std::prev(it), deloop); in_a_run = false; } current_azimuth = it->azimuth; } return in_a_run; }; double const last_azimuth = incidences.back().azimuth; if (angles_equal(incidences.front().azimuth, last_azimuth)) { // The cyclic list contains a run of equal azimuths which straddles the "end". // This means that we must skip the part of this run on the "begin" side on the // first pass and handle it once we've traversed the remainder of the list. bool processed = false; ///< Whether we've cleared the straddling run. double previous_azimuth = last_azimuth; IncIt straddling_run_last; for (auto it = incidences.begin(); it != incidences.end(); ++it) { if (!angles_equal(it->azimuth, previous_azimuth)) { straddling_run_last = std::prev(it); process_runs(it, incidences.end()); processed = true; break; } previous_azimuth = it->azimuth; } if (processed) { // Find the first element of the straddling run. auto it = std::prev(incidences.end()); while (angles_equal(it->azimuth, last_azimuth)) { --it; } ++it; // Now we're at the start of the straddling run. _reglueTangentFan(vertex, it, straddling_run_last, deloop); } else { // We never encountered anything outside of the straddling run: reglue everything. _reglueTangentFan(vertex, incidences.begin(), std::prev(incidences.end()), deloop); } } else if (process_runs(incidences.begin(), incidences.end())) { // Our run got rudely interrupted by the end of the container; reglue till the end. _reglueTangentFan(vertex, run_begin, std::prev(incidences.end()), deloop); } } /** * \brief Regularize a fan of mutually tangent edges emanating from a vertex. * * This function compares the tangent edges pairwise and ensures that the sequence of their * incidences to the vertex ends up being sorted by the ultimate direction in which the * emanating edges fan out, in the counterclockwise order. * * If a partial or complete overlap between edges is detected, these edges are reglued. * * \param vertex The vertex from which the fan emanates. * \param first An iterator pointing to the first incidence in the fan. * \param last An iterator pointing to the last incidence in the fan. * NOTE: This iterator must point to the actual last incidence, not "past" it. * The reason is that we're iterating over a cyclic collection, so there * isn't really a meaningful end. * \param deloop Whether loops that don't enclose any area should be detached. */ template void PlanarGraph::_reglueTangentFan(typename PlanarGraph::Vertex &vertex, typename PlanarGraph::IncIt const &first, typename PlanarGraph::IncIt const &last, bool deloop) { // Search all pairs (triangular pattern), skipping invalid incidences. for (auto it = first; it != last; it = vertex.cyclicNextIncidence(it)) { if (it->invalid) { continue; } for (auto is = vertex.cyclicNextIncidence(it); true; is = vertex.cyclicNextIncidence(is)) { if (!is->invalid && _compareAndReglue(vertex, &(*it), &(*is), deloop)) { // Swap the incidences, effectively implementing "bubble sort". std::swap(*it, *is); } if (is == last) { break; } } } } /** * \brief Compare a pair of edges emanating from the same vertex in the same direction. * * If the edges overlap in part or in full, they get reglued, which means that the topology * of the graph may get modified. Otherwise, if the detailed comparison shows that the edges * aren't correctly ordered around the vertex (because the second edge deviates to the right * instead of to the left of the first, when looking away from the vertex), then the function * will return true, signalling that the incidences should be swapped. * * \param vertex The vertex where the mutually tangent paths meet. * \param first The incidence appearing as the first one in the provisional cyclic order. * \param second The incidence appearing as the second one in the provisional cyclic order. * \param deloop Whether to detach collapsed loops (backtracks) which don't enclose any area. * \return Whether the incidences should be swapped. */ template bool PlanarGraph::_compareAndReglue(typename PlanarGraph::Vertex &vertex, typename PlanarGraph::Incidence *first, typename PlanarGraph::Incidence *second, bool deloop) { if (first->index == second->index) { return _reglueTeardrop(vertex, first, second, deloop); } // Get paths corresponding to the edges but travelling away from the vertex. auto first_path_out = getOutgoingPath(first); auto second_path_out = getOutgoingPath(second); auto split = parting_point(first_path_out, second_path_out, _precision); if (are_near(split.point(), vertex.point(), _precision)) { // Paths deviate immediately, so no gluing is needed. The incidences should // be swapped if the first edge path departs to the left of the second one. return deviatesLeft(first_path_out, second_path_out); } // Determine the nature of the initial overlap between the paths. bool const till_end_of_1st = are_near(split.point(), first_path_out.finalPoint(), _precision); bool const till_end_of_2nd = are_near(split.point(), second_path_out.finalPoint(), _precision); if (till_end_of_1st && till_end_of_2nd) { // Paths coincide completely. _mergeCoincidingEdges(first, second); } else if (till_end_of_1st) { // Paths coincide until the end of the 1st one, which however isn't the end of the // 2nd one; for example, the first one could be the vertical riser of the letter L // whereas the second one – the entire letter stroke. _mergeShorterLonger(vertex, first, second, split.second); } else if (till_end_of_2nd) { // The same but with with the second edge shorter than the first one. _mergeShorterLonger(vertex, second, first, split.first); } else { // A Y-shaped split. _mergeWyeConfiguration(vertex, first, second, split); } return false; // We've glued so no need to swap anything. } /** * \brief Analyze a loop path a with self-tangency at the attachment point (a teardrop). * * The following steps are taken: * \li If the loop encloses zero area and \c deloop is true, the loop is detached. * \li If the two arms of the loop split out immediately, the loop is left alone and we * only check whether the incidences should be swapped. * \li If the loop overlaps itself near the vertex, resembling a lasso, we split it into * a shorter simple path and a smaller loop attached to the end of the shorter path. * * \param vertex The vertex at which the teardrop originates. * \param first The first incidence of the loop to the vertex. * \param second The second incidence of the loop to the vertex. * \param deloop Whether the loop should be removed if it doesn't enclose any area * (i.e., the path exactly backtracks on itself). * \return Whether the two incidences of the loop to the vertex should be swapped. */ template bool PlanarGraph::_reglueTeardrop(typename PlanarGraph::Vertex &vertex, typename PlanarGraph::Incidence *first, typename PlanarGraph::Incidence *second, bool deloop) { // Calculate the area enclosed by the teardrop. // The convention is that the unit circle (cos(t), sint(t)), t from 0 to 2pi, // encloses an area of +pi. auto &edge = _edges[first->index]; Path loop = edge.path; loop.close(); double signed_area = closedPathArea(loop); if (deloop && are_near(signed_area, 0.0, _precision)) { edge.detach(); _throwAway(&vertex, first); _throwAway(&vertex, second); return false; } auto split = parting_point(loop, loop.reversed(), _precision); if (are_near(split.point(), vertex.point(), _precision)) { // The loop spreads out immediately. We simply check if the incidences should be swapped. // We want them to be ordered such that the signed area encircled by the path going out // at the first incidence and coming back at the second (with this orientation) is > 0. return (first->sign == Incidence::START) ^ (signed_area > 0.0); } // The loop encloses a nonzero area, but the two branches don't separate at the starting // point. Instead, they travel together for a while before they split like a lasso. _reglueLasso(vertex, first, second, split); return false; } /** * \brief Reglue a lasso-shaped loop, separating it into the "free rope" and the "hoop". * * The lasso is an edge looping back to the same vertex, where the closed path encloses * a non-zero area, but its two branches don't separate at the starting point. Instead, * they travel together for a while (forming the doubled-up "free rope") before they * split like a lasso. This function cuts the lasso at the split point: * \code{.unparsed} * ____ ____ * / \ / \ * VERTEX =====< | ==> VERTEX ------ NEW + NEW < | * \____/ (lasso) (rope) \____/ (hoop) * * \endcode * * \param vertex A reference to the vertex where the lasso is attached. * \param first The first incidence of the lasso to the vertex. * \param second The second incidence of the lasso to the vertex. * \param split The point where the free rope of the lasso ends and the hoop begins. */ template void PlanarGraph::_reglueLasso(typename PlanarGraph::Vertex &vertex, typename PlanarGraph::Incidence *first, typename PlanarGraph::Incidence *second, PathIntersection const &split) { unsigned lasso = first->index; // Create the "free rope" path. auto rope = _edges[lasso].path.portion(PATH_START, split.first); rope.setInitial(vertex.point()); rope.setFinal(split.point()); double const rope_final_backward_azimuth = _getAzimuth(-rope.finalUnitTangent()); // Compute the new label of the rope edge. auto oriented_as_loop = _edges[lasso].label; auto reversed = oriented_as_loop; reversed.onReverse(); oriented_as_loop.onMergeWith(reversed); // Insert the rope and its endpoint. unsigned const rope_index = _edges.size(); auto &rope_edge = _edges.emplace_back(std::move(rope), std::move(oriented_as_loop)); auto const new_split_vertex = _ensureVertexAt(split.point()); // Reuse lasso's first incidence as the incidence to the rope (azimuth can stay). first->index = rope_index; first->sign = Incidence::START; // Connect the rope to the newly created split vertex. new_split_vertex->_addIncidence(rope_index, rope_final_backward_azimuth, Incidence::END); rope_edge.start = &vertex; rope_edge.end = new_split_vertex; // Insert the hoop auto hoop = _edges[lasso].path.portion(split.first, _reversePathTime(split.second, _edges[lasso].path)); hoop.setInitial(split.point()); hoop.setFinal(split.point()); insertEdge(std::move(hoop), EL(_edges[lasso].label)); // Detach the original lasso edge and mark the second incidence for cleanup. _edges[lasso].detach(); _throwAway(&vertex, second); } /** * \brief Completely coallesce two fully overlapping edges. * * In practice, the first edge stays and the second one gets detached from the graph. * * \param first An iterator to the first edge's incidence to a common vertex. * \param second An iterator to the second edge's incidence to a common vertex. */ template void PlanarGraph::_mergeCoincidingEdges(typename PlanarGraph::Incidence *first, typename PlanarGraph::Incidence *second) { auto &surviver = _edges[first->index]; auto &casualty = _edges[second->index]; auto other_label = casualty.label; if (first->sign != second->sign) { // Logically reverse the label before merging. other_label.onReverse(); } surviver.label.onMergeWith(other_label); // Mark both incidences of the second edge as junk and detach it. auto [start_vertex, start_inc] = getIncidence(second->index, Incidence::START); _throwAway(start_vertex, start_inc); auto [end_vertex, end_inc] = getIncidence(second->index, Incidence::END); _throwAway(end_vertex, end_inc); casualty.detach(); } /** * \brief Merge a longer edge with a shorter edge that overlaps it. * * In practice, the shorter edge remains unchanged and the longer one is trimmed to * become just the part extending past the shorter one. * * \param vertex The vertex where the overlap starts. * \param shorter The incidence of the shorter edge to the common vertex. * \param longer The incidence of the longer edge to the common vertex. * \param time_on_longer The PathTime on the longer edge at which it passes through * the endpoint of the shorter edge. */ template void PlanarGraph::_mergeShorterLonger(typename PlanarGraph::Vertex &vertex, typename PlanarGraph::Incidence *shorter, typename PlanarGraph::Incidence *longer, PathTime const &time_on_longer) { auto &shorter_edge = _edges[shorter->index]; auto &longer_edge = _edges[longer->index]; // Get the vertices at the far ends of both edges. auto shorter_far_end = (shorter->sign == Incidence::START) ? shorter_edge.end : shorter_edge.start; /// Whether the longer edge heads out of the vertex. bool const longer_out = (longer->sign == Incidence::START); auto longer_far_end = longer_out ? longer_edge.end : longer_edge.start; // Copy the longer edge's label and merge with that of the shorter. auto longer_label = longer_edge.label; if (shorter->sign != longer->sign) { longer_label.onReverse(); } shorter_edge.label.onMergeWith(longer_label); // Create the trimmed path (longer minus shorter). Path trimmed; double trimmed_departure_azimuth; if (longer_out) { trimmed = longer_edge.path.portion(time_on_longer, _pathEnd(longer_edge.path)); longer_edge.start = shorter_far_end; trimmed.setInitial(shorter_far_end->point()); trimmed.setFinal(longer_far_end->point()); trimmed_departure_azimuth = _getAzimuth(trimmed.initialUnitTangent()); } else { trimmed = longer_edge.path.portion(PATH_START, _reversePathTime(time_on_longer, longer_edge.path)); longer_edge.end = shorter_far_end; trimmed.setInitial(longer_far_end->point()); trimmed.setFinal(shorter_far_end->point()); trimmed_departure_azimuth = _getAzimuth(-trimmed.finalUnitTangent()); } // Set the trimmed path as the new path of the longer edge and set up the incidences: longer_edge.path = std::move(trimmed); shorter_far_end->_addIncidence(longer->index, trimmed_departure_azimuth, longer->sign); // Throw away the old start incidence of the longer edge. _throwAway(&vertex, longer); } /** * \brief Merge a pair of partially overlapping edges, producing a Y-split at a new vertex. * * This topological modification is performed by inserting a new vertex at the three-way * point (where the two paths separate) and clipping the original edges to that point. * In this way, the original edges become the "arms" of the Y-shape. In addition, a new * edge is inserted, forming the "stem" of the Y. * * \param vertex The vertex from which the partially overlapping edges originate (bottom of Y). * \param first The incidence to the first edge (whose path is the stem and one arm of the Y). * \param second The incidence to the second edge (stem and the other arm of the Y). * \param fork The splitting point of the two paths. */ template void PlanarGraph::_mergeWyeConfiguration(typename PlanarGraph::Vertex &vertex, typename PlanarGraph::Incidence *first, typename PlanarGraph::Incidence *second, PathIntersection const &fork) { bool const first_is_out = (first->sign == Incidence::START); bool const second_is_out = (second->sign == Incidence::START); auto &first_edge = _edges[first->index]; auto &second_edge = _edges[second->index]; // Calculate the path forming the stem of the Y: auto stem_path = getOutgoingPath(first).portion(PATH_START, fork.first); stem_path.setInitial(vertex.point()); stem_path.setFinal(fork.point()); /// A closure to clip the path of an original edge to the fork point. auto const clip_to_fork = [&](PathTime const &t, Edge &e, bool out) { if (out) { // Trim from time to end e.path = e.path.portion(t, _pathEnd(e.path)); e.path.setInitial(fork.point()); } else { // Trim from reverse-end to reverse-time e.path = e.path.portion(PATH_START, _reversePathTime(t, e.path)); e.path.setFinal(fork.point()); } }; /// A closure to find the departing azimuth of an edge at the fork point. auto const departing_azimuth = [&](Edge const &e, bool out) -> double { return _getAzimuth((out) ? e.path.initialUnitTangent() : -e.path.finalUnitTangent()); }; // Clip the paths obtaining the arms of the Y. clip_to_fork(fork.first, first_edge, first_is_out); clip_to_fork(fork.second, second_edge, second_is_out); // Create the fork vertex and set up its incidences. auto const fork_vertex = _ensureVertexAt(fork.point()); fork_vertex->_addIncidence(first->index, departing_azimuth(first_edge, first_is_out), first->sign); fork_vertex->_addIncidence(second->index, departing_azimuth(second_edge, second_is_out), second->sign); // Repoint the ends of the edges that were clipped (first_is_out ? first_edge.start : first_edge.end) = fork_vertex; (second_is_out ? second_edge.start : second_edge.end) = fork_vertex; /// A closure to get a consistently oriented label of an edge. auto upwards_oriented_label = [&](Edge const &e, bool out) -> EL { auto label = e.label; if (!out) { label.onReverse(); } return label; }; auto stem_label = upwards_oriented_label(first_edge, first_is_out); stem_label.onMergeWith(upwards_oriented_label(second_edge, second_is_out)); auto stem_departure_from_fork = _getAzimuth(-stem_path.finalUnitTangent()); // Insert the stem of the Y-configuration. unsigned const stem_index = _edges.size(); auto &stem_edge = _edges.emplace_back(std::move(stem_path), std::move(stem_label)); stem_edge.start = &vertex; stem_edge.end = fork_vertex; // Set up the incidences. fork_vertex->_addIncidence(stem_index, stem_departure_from_fork, Incidence::END); first->index = stem_index; first->sign = Incidence::START; _throwAway(&vertex, second); } template typename PlanarGraph::Incidence* PlanarGraph::nextIncidence(typename PlanarGraph::VertexIterator const &vertex, double azimuth, bool clockwise) const { auto &incidences = vertex._incidences; Incidence *result = nullptr; if (incidences.empty()) { return result; } // Normalize azimuth to the interval [-pi; pi]. auto angle = Angle(azimuth); if (clockwise) { // Go backwards and find a lower bound auto it = std::find_if(incidences.rbegin(), incidences.rend(), [=](auto inc) -> bool { return inc.azimuth <= angle; }); if (it == incidences.rend()) { // azimuth is lower than the azimuths of all incidences; // going clockwise we wrap back to the highest azimuth (last incidence). return &incidences.back(); } result = &(*it); } else { auto it = std::find_if(incidences.begin(), incidences.end, [=](auto inc) -> bool { return inc.azimuth >= angle; }); if (it == incidences.end()) { // azimuth is higher than the azimuths of all incidences; // going counterclockwise we wrap back to the lowest azimuth. return &incidences.front(); } result = &(*it); } return result; } /** Return the signed area enclosed by a closed path. */ template double PlanarGraph::closedPathArea(Path const &path) { double area; Point _; centroid(path.toPwSb(), _, area); return -area; // Our convention is that the Y-axis points up } /** \brief Determine whether the first path deviates to the left of the second. * * The two paths are assumed to have identical or nearly identical starting points * but not an overlapping initial portion. The concept of "left" is based on the * y-axis pointing up. * * \param first The first path. * \param second The second path. * * \return True if the first path deviates towards the left of the second; * False if the first path deviates towards the right of the second. */ template bool PlanarGraph::deviatesLeft(Path const &first, Path const &second) { auto start = middle_point(first.initialPoint(), second.initialPoint()); auto tangent_between = middle_point(first.initialUnitTangent(), second.initialUnitTangent()); if (tangent_between.isZero()) { return false; } auto tangent_line = Line::from_origin_and_vector(start, tangent_between); // Find the first non-degenerate curves on both paths std::unique_ptr c[2]; auto const find_first_nondegen = [](std::unique_ptr &pointer, Path const &path) { for (auto const &c : path) { if (!c.isDegenerate()) { pointer.reset(c.duplicate()); return; } } }; find_first_nondegen(c[0], first); find_first_nondegen(c[1], second); if (!c[0] || !c[1]) { return false; } // Find the bounding boxes Rect const bounding_boxes[] { c[0]->boundsExact(), c[1]->boundsExact() }; // For a bounding box, find the corner that goes the furthest in the direction of the // tangent vector. auto const furthest_corner = [&](Rect const &r) -> unsigned { Coord max_dot = dot(r.corner(0) - start, tangent_between); unsigned result = 0; for (unsigned i = 1; i < 4; i++) { auto current_dot = dot(r.corner(i), tangent_between); if (current_dot > max_dot) { max_dot = current_dot; result = i; } else if (current_dot == max_dot) { // Disambiguate based on proximity to the tangent line. auto const offset = start + tangent_between; if (distance(offset, r.corner(i)) < distance(offset, r.corner(result))) { result = i; } } } return result; }; // Calculate the corner points overlooking the "rift" between the paths. Point corner_points[2]; for (size_t i : {0, 1}) { corner_points[i] = bounding_boxes[i].corner(furthest_corner(bounding_boxes[i])); } // Find a vantage point from which we can best observe the splitting paths. Point vantage_point; bool found = false; if (corner_points[0] != corner_points[1]) { auto line_connecting_corners = Line(corner_points[0], corner_points[1]); auto xing = line_connecting_corners.intersect(tangent_line); if (!xing.empty()) { vantage_point = xing[0].point(); found = true; } } if (!found) { vantage_point = tangent_line.pointAt(tangent_line.timeAtProjection(corner_points[0])); } // Move to twice as far in the direction of the vantage point. vantage_point += vantage_point - start; // Find the points on both curves that are nearest to the vantage point. Coord nearest[2]; for (size_t i : {0, 1}) { nearest[i] = c[i]->nearestTime(vantage_point); } // Clip to the nearest points and examine the closed contour. Path closed_contour(start); closed_contour.setStitching(true); closed_contour.append(c[0]->portion(0, nearest[0])); closed_contour = closed_contour.reversed(); closed_contour.setStitching(true); closed_contour.append(c[1]->portion(0, nearest[1])); closed_contour.close(); return !path_direction(closed_contour); // Reverse to match the convention that y-axis is up. } } // namespace Geom #endif // LIB2GEOM_SEEN_PLANAR_GRAPH_H /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :