diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:57:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 11:57:42 +0000 |
commit | 61f3ab8f23f4c924d455757bf3e65f8487521b5a (patch) | |
tree | 885599a36a308f422af98616bc733a0494fe149a /src/2geom/planar-graph.h | |
parent | Initial commit. (diff) | |
download | lib2geom-upstream.tar.xz lib2geom-upstream.zip |
Adding upstream version 1.3.upstream/1.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/2geom/planar-graph.h')
-rw-r--r-- | src/2geom/planar-graph.h | 1252 |
1 files changed, 1252 insertions, 0 deletions
diff --git a/src/2geom/planar-graph.h b/src/2geom/planar-graph.h new file mode 100644 index 0000000..fb5f1ac --- /dev/null +++ b/src/2geom/planar-graph.h @@ -0,0 +1,1252 @@ +/** @file PlanarGraph – a graph geometrically embedded in the plane. + */ +/* + * Authors: + * Rafał Siejakowski <rs@rs-math.net> + * + * 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 <algorithm> +#include <iterator> +#include <list> + +#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<typename EdgeLabel> +#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<Incidence>::iterator; + using IncConstIt = typename std::list<Incidence>::const_iterator; + + /** Represents the vertex of a planar graph. */ + class Vertex + { + private: + Point const _position; ///< Geometric position of the vertex. + std::list<Incidence> _incidences; ///< List of incidences of edges to this vertex. + unsigned mutable _flags = 0; ///< User-settable flags. + + inline static Point::LexLess<X> 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<EdgeLabel>; + }; + using VertexIterator = typename std::list<Vertex>::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<Edge>::iterator; + using EdgeConstIterator = typename std::vector<Edge>::const_iterator; + +private: + double const _precision; ///< Numerical epsilon for vertex clumping. + std::list<Vertex> _vertices; ///< Vertices of the graph. + std::vector<Edge> _edges; ///< Edges of the graph. + std::vector< std::pair<Vertex *, Incidence *> > _junk; ///< Incidences that should be purged. + bool _regularized = true; // An empty graph is (trivially) regularized. + +public: + PlanarGraph(Coord precision = EPSILON) + : _precision{precision} + { + } + + std::list<Vertex> const &getVertices() const { return _vertices; } + std::vector<Edge> 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<EdgeLabel>(edge)); + } + unsigned insertDetached(Path const &path, EdgeLabel &&edge = EdgeLabel()) + { + return insertDetached(Path(path), std::forward<EdgeLabel>(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<Vertex *, Incidence *> + 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 EL> +typename PlanarGraph<EL>::Vertex *PlanarGraph<EL>::_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<typename EdgeLabel> +unsigned PlanarGraph<EdgeLabel>::insertEdge(Path &&path, EdgeLabel &&label) +{ + unsigned edge_index = _edges.size(); + auto &inserted = _edges.emplace_back(std::forward<Path>(path), + std::forward<EdgeLabel>(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<typename EdgeLabel> +unsigned PlanarGraph<EdgeLabel>::insertDetached(Path &&path, EdgeLabel &&label) +{ + unsigned edge_index = _edges.size(); + auto &inserted = _edges.emplace_back(std::forward<Path>(path), + std::forward<EdgeLabel>(label)); + inserted.detached = true; + inserted.inserted_as_detached = true; + return edge_index; +} + +/** Remove incidences previously marked as junk. */ +template<typename EdgeLabel> +void PlanarGraph<EdgeLabel>::_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<typename EdgeLabel> +void PlanarGraph<EdgeLabel>::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<typename EdgeLabel> +void PlanarGraph<EdgeLabel>::_regularizeVertex(typename PlanarGraph<EdgeLabel>::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<typename EL> +void PlanarGraph<EL>::_reglueTangentFan(typename PlanarGraph<EL>::Vertex &vertex, + typename PlanarGraph<EL>::IncIt const &first, + typename PlanarGraph<EL>::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<typename EL> +bool PlanarGraph<EL>::_compareAndReglue(typename PlanarGraph<EL>::Vertex &vertex, + typename PlanarGraph<EL>::Incidence *first, + typename PlanarGraph<EL>::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<typename EL> +bool PlanarGraph<EL>::_reglueTeardrop(typename PlanarGraph<EL>::Vertex &vertex, + typename PlanarGraph<EL>::Incidence *first, + typename PlanarGraph<EL>::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<typename EL> +void PlanarGraph<EL>::_reglueLasso(typename PlanarGraph<EL>::Vertex &vertex, + typename PlanarGraph<EL>::Incidence *first, + typename PlanarGraph<EL>::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<typename EL> +void PlanarGraph<EL>::_mergeCoincidingEdges(typename PlanarGraph<EL>::Incidence *first, + typename PlanarGraph<EL>::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<typename EL> +void PlanarGraph<EL>::_mergeShorterLonger(typename PlanarGraph<EL>::Vertex &vertex, + typename PlanarGraph<EL>::Incidence *shorter, + typename PlanarGraph<EL>::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<typename EL> +void PlanarGraph<EL>::_mergeWyeConfiguration(typename PlanarGraph<EL>::Vertex &vertex, + typename PlanarGraph<EL>::Incidence *first, + typename PlanarGraph<EL>::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 EL> +typename PlanarGraph<EL>::Incidence* +PlanarGraph<EL>::nextIncidence(typename PlanarGraph<EL>::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<typename EL> +double PlanarGraph<EL>::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<typename EL> +bool PlanarGraph<EL>::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<Curve> c[2]; + auto const find_first_nondegen = [](std::unique_ptr<Curve> &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 : |