summaryrefslogtreecommitdiffstats
path: root/src/2geom/intersection-graph.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/2geom/intersection-graph.cpp')
-rw-r--r--src/2geom/intersection-graph.cpp535
1 files changed, 535 insertions, 0 deletions
diff --git a/src/2geom/intersection-graph.cpp b/src/2geom/intersection-graph.cpp
new file mode 100644
index 0000000..524267e
--- /dev/null
+++ b/src/2geom/intersection-graph.cpp
@@ -0,0 +1,535 @@
+/**
+ * \file
+ * \brief Intersection graph for Boolean operations
+ *//*
+ * Authors:
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright 2015 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.
+ */
+
+#include <2geom/intersection-graph.h>
+#include <2geom/path.h>
+#include <2geom/pathvector.h>
+#include <2geom/utils.h>
+#include <iostream>
+#include <iterator>
+
+namespace Geom {
+
+/// Function object for comparing intersection vertices based on the intersection time.
+struct PathIntersectionGraph::IntersectionVertexLess {
+ bool operator()(IntersectionVertex const &a, IntersectionVertex const &b) const {
+ return a.pos < b.pos;
+ }
+};
+
+PathIntersectionGraph::PathIntersectionGraph(PathVector const &a, PathVector const &b, Coord precision)
+ : _graph_valid(true)
+{
+ _pv[0] = a;
+ _pv[1] = b;
+
+ if (a.empty() || b.empty()) return;
+
+ _prepareArguments();
+ bool has_intersections = _prepareIntersectionLists(precision);
+ if (!has_intersections) return;
+
+ _assignEdgeWindingParities(precision);
+
+ // If a path has only degenerate intersections, assign its status now.
+ // This protects against later accidentally picking a point for winding
+ // determination that is exactly at a removed intersection.
+ _assignComponentStatusFromDegenerateIntersections();
+ _removeDegenerateIntersections();
+ if (_graph_valid) {
+ _verify();
+ }
+}
+
+/** Prepare the operands stored in PathIntersectionGraph::_pv by closing all of their constituent
+ * paths and removing degenerate segments from them.
+ */
+void PathIntersectionGraph::_prepareArguments()
+{
+ // all paths must be closed, otherwise we will miss some intersections
+ for (auto & w : _pv) {
+ for (auto & i : w) {
+ i.close();
+ }
+ }
+ // remove degenerate segments
+ for (auto & w : _pv) {
+ for (std::size_t i = w.size(); i > 0; --i) {
+ if (w[i-1].empty()) {
+ w.erase(w.begin() + (i-1));
+ continue;
+ }
+ for (std::size_t j = w[i-1].size(); j > 0; --j) {
+ if (w[i-1][j-1].isDegenerate()) {
+ w[i-1].erase(w[i-1].begin() + (j-1));
+ }
+ }
+ }
+ }
+}
+
+/** @brief Compute the lists of intersections between the constituent paths of both operands.
+ * @param precision – the precision setting for the sweepline algorithm.
+ * @return Whether any intersections were found.
+ */
+bool PathIntersectionGraph::_prepareIntersectionLists(Coord precision)
+{
+ std::vector<PVIntersection> pxs = _pv[0].intersect(_pv[1], precision);
+ // NOTE: this early return means that the path data structures will not be created
+ // if there are no intersections at all!
+ if (pxs.empty()) return false;
+
+ // prepare intersection lists for each path component
+ for (unsigned w = 0; w < 2; ++w) {
+ for (std::size_t i = 0; i < _pv[w].size(); ++i) {
+ _components[w].push_back(new PathData(w, i));
+ }
+ }
+
+ // create intersection vertices
+ for (auto & px : pxs) {
+ IntersectionVertex *xa, *xb;
+ xa = new IntersectionVertex();
+ xb = new IntersectionVertex();
+ //xa->processed = xb->processed = false;
+ xa->which = 0; xb->which = 1;
+ xa->pos = px.first;
+ xb->pos = px.second;
+ xa->p = xb->p = px.point();
+ xa->neighbor = xb;
+ xb->neighbor = xa;
+ xa->next_edge = xb->next_edge = OUTSIDE;
+ xa->defective = xb->defective = false;
+ _xs.push_back(xa);
+ _xs.push_back(xb);
+ _components[0][xa->pos.path_index].xlist.push_back(*xa);
+ _components[1][xb->pos.path_index].xlist.push_back(*xb);
+ }
+
+ // sort intersections in each component according to time value
+ for (auto & _component : _components) {
+ for (std::size_t i = 0; i < _component.size(); ++i) {
+ _component[i].xlist.sort(IntersectionVertexLess());
+ }
+ }
+
+ return true;
+}
+
+/** Determine whether path portions between consecutive intersections lie inside or outside
+ * of the other path-vector.
+ */
+void PathIntersectionGraph::_assignEdgeWindingParities(Coord precision)
+{
+ for (unsigned w = 0; w < 2; ++w) {
+ unsigned ow = (w+1) % 2; ///< The index of the other operand
+
+ for (unsigned li = 0; li < _components[w].size(); ++li) { // Traverse all paths in the component
+ IntersectionList &xl = _components[w][li].xlist;
+ for (ILIter i = xl.begin(); i != xl.end(); ++i) { // Traverse all intersections in the path
+ ILIter n = cyclic_next(i, xl);
+ std::size_t pi = i->pos.path_index;
+
+ /// Path time interval from the current crossing to the next one
+ PathInterval ival = forward_interval(i->pos, n->pos, _pv[w][pi].size());
+ PathTime mid = ival.inside(precision);
+
+ Point wpoint = _pv[w][pi].pointAt(mid);
+ _winding_points.push_back(wpoint);
+ int wdg = _pv[ow].winding(wpoint);
+ if (wdg % 2) {
+ i->next_edge = INSIDE;
+ } else {
+ i->next_edge = OUTSIDE;
+ }
+ }
+ }
+ }
+}
+
+/** Detect the situation where a path is either entirely inside or entirely outside of the other
+ * path-vector and set the status flag accordingly.
+ */
+void PathIntersectionGraph::_assignComponentStatusFromDegenerateIntersections()
+{
+ for (auto & _component : _components) {
+ for (unsigned li = 0; li < _component.size(); ++li) {
+ IntersectionList &xl = _component[li].xlist;
+ bool has_in = false;
+ bool has_out = false;
+ for (auto & i : xl) {
+ has_in |= (i.next_edge == INSIDE);
+ has_out |= (i.next_edge == OUTSIDE);
+ }
+ if (has_in && !has_out) {
+ _component[li].status = INSIDE;
+ }
+ if (!has_in && has_out) {
+ _component[li].status = OUTSIDE;
+ }
+ }
+ }
+}
+
+/** Remove intersections that don't change between in/out.
+ *
+ * In general, a degenerate intersection can happen at a point where
+ * two shapes "kiss" (are tangent) but do not cross into each other.
+ */
+void PathIntersectionGraph::_removeDegenerateIntersections()
+{
+ for (auto & _component : _components) {
+ for (unsigned li = 0; li < _component.size(); ++li) {
+ IntersectionList &xl = _component[li].xlist;
+ for (ILIter i = xl.begin(); i != xl.end();) {
+ ILIter n = cyclic_next(i, xl);
+ if (i->next_edge == n->next_edge) { // Both edges inside or both outside
+ bool last_node = (i == n); ///< Whether this is the last remaining crossing.
+ ILIter nn = _getNeighbor(n);
+ IntersectionList &oxl = _getPathData(nn).xlist;
+
+ // When exactly 3 out of 4 edges adjacent to an intersection
+ // have the same winding, we have a defective intersection,
+ // which is neither degenerate nor normal. Those can occur in paths
+ // that contain overlapping segments.
+ if (cyclic_prior(nn, oxl)->next_edge != nn->next_edge) {
+ // Not a backtrack - set the defective flag.
+ _graph_valid = false;
+ n->defective = true;
+ nn->defective = true;
+ ++i;
+ continue;
+ }
+ // Erase the degenerate or defective crossings
+ oxl.erase(nn);
+ xl.erase(n);
+ if (last_node) break;
+ } else {
+ ++i;
+ }
+ }
+ }
+ }
+}
+
+/** Verify that all paths contain an even number of intersections and that
+ * the intersection graph does not contain leaves (degree one vertices).
+ */
+void PathIntersectionGraph::_verify()
+{
+#ifndef NDEBUG
+ for (auto & _component : _components) {
+ for (unsigned li = 0; li < _component.size(); ++li) {
+ IntersectionList &xl = _component[li].xlist;
+ assert(xl.size() % 2 == 0);
+ for (ILIter i = xl.begin(); i != xl.end(); ++i) {
+ ILIter j = cyclic_next(i, xl);
+ assert(i->next_edge != j->next_edge);
+ }
+ }
+ }
+#endif
+}
+
+PathVector PathIntersectionGraph::getUnion()
+{
+ PathVector result = _getResult(false, false);
+ _handleNonintersectingPaths(result, 0, false);
+ _handleNonintersectingPaths(result, 1, false);
+ return result;
+}
+
+PathVector PathIntersectionGraph::getIntersection()
+{
+ PathVector result = _getResult(true, true);
+ _handleNonintersectingPaths(result, 0, true);
+ _handleNonintersectingPaths(result, 1, true);
+ return result;
+}
+
+PathVector PathIntersectionGraph::getAminusB()
+{
+ PathVector result = _getResult(false, true);
+ _handleNonintersectingPaths(result, 0, false);
+ _handleNonintersectingPaths(result, 1, true);
+ return result;
+}
+
+PathVector PathIntersectionGraph::getBminusA()
+{
+ PathVector result = _getResult(true, false);
+ _handleNonintersectingPaths(result, 1, false);
+ _handleNonintersectingPaths(result, 0, true);
+ return result;
+}
+
+PathVector PathIntersectionGraph::getXOR()
+{
+ PathVector r1, r2;
+ r1 = getAminusB();
+ r2 = getBminusA();
+ std::copy(r2.begin(), r2.end(), std::back_inserter(r1));
+ return r1;
+}
+
+std::size_t PathIntersectionGraph::size() const
+{
+ std::size_t result = 0;
+ for (std::size_t i = 0; i < _components[0].size(); ++i) {
+ result += _components[0][i].xlist.size();
+ }
+ return result;
+}
+
+std::vector<Point> PathIntersectionGraph::intersectionPoints(bool defective) const
+{
+ std::vector<Point> result;
+
+ for (std::size_t i = 0; i < _components[0].size(); ++i) {
+ for (const auto & j : _components[0][i].xlist) {
+ if (j.defective == defective) {
+ result.push_back(j.p);
+ }
+ }
+ }
+ return result;
+}
+
+void PathIntersectionGraph::fragments(PathVector &in, PathVector &out) const
+{
+ typedef boost::ptr_vector<PathData>::const_iterator PIter;
+ for (unsigned w = 0; w < 2; ++w) {
+ for (PIter li = _components[w].begin(); li != _components[w].end(); ++li) {
+ for (CILIter k = li->xlist.begin(); k != li->xlist.end(); ++k) {
+ CILIter n = cyclic_next(k, li->xlist);
+ // TODO: investigate why non-contiguous paths are sometimes generated here
+ Path frag(k->p);
+ frag.setStitching(true);
+ PathInterval ival = forward_interval(k->pos, n->pos, _pv[w][k->pos.path_index].size());
+ _pv[w][k->pos.path_index].appendPortionTo(frag, ival, k->p, n->p);
+ if (k->next_edge == INSIDE) {
+ in.push_back(frag);
+ } else {
+ out.push_back(frag);
+ }
+ }
+ }
+ }
+}
+
+/** @brief Compute the partial result of a boolean operation by looking at components containing
+ * intersections and stitching the correct path portions between them, depending on the truth
+ * table of the operation.
+ *
+ * @param enter_a – whether the path portions contained inside operand A should be part of the boundary
+ * of the boolean operation's result.
+ * @param enter_b – whether the path portions contained inside operand B should be part of the boundary
+ * of the boolean operation's result.
+ *
+ * These two flags completely determine how to resolve the crossings when building the result
+ * and therefore encode which boolean operation we are performing. For example, the boolean intersection
+ * corresponds to enter_a == true and enter_b == true, as can be seen by looking at a Venn diagram.
+ */
+PathVector PathIntersectionGraph::_getResult(bool enter_a, bool enter_b)
+{
+ PathVector result;
+ if (_xs.empty()) return result;
+
+ // Create the list of intersections to process
+ _ulist.clear();
+ for (auto & _component : _components) {
+ for (auto & li : _component) {
+ for (auto & k : li.xlist) {
+ _ulist.push_back(k);
+ }
+ }
+ }
+
+ unsigned n_processed = 0;
+
+ while (true) {
+ // get unprocessed intersection
+ if (_ulist.empty()) break;
+ IntersectionVertex &iv = _ulist.front();
+ unsigned w = iv.which;
+ ILIter i = _components[w][iv.pos.path_index].xlist.iterator_to(iv);
+
+ result.push_back(Path(i->p));
+ result.back().setStitching(true);
+ bool reverse = false; ///< Whether to traverse the current component in the backwards direction.
+ while (i->_proc_hook.is_linked()) {
+ ILIter prev = i;
+ std::size_t pi = i->pos.path_index; ///< Index of the path in its PathVector
+ // determine which direction to go
+ // union: always go outside
+ // intersection: always go inside
+ // a minus b: go inside in b, outside in a
+ // b minus a: go inside in a, outside in b
+ if (w == 0) { // The path we're on is a part of A
+ reverse = (i->next_edge == INSIDE) ^ enter_a;
+ } else { // The path we're on is a part of B
+ reverse = (i->next_edge == INSIDE) ^ enter_b;
+ }
+
+ // get next intersection
+ if (reverse) {
+ i = cyclic_prior(i, _components[w][pi].xlist);
+ } else {
+ i = cyclic_next(i, _components[w][pi].xlist);
+ }
+
+ // append portion of path to the result
+ PathInterval ival = PathInterval::from_direction(
+ prev->pos.asPathTime(), i->pos.asPathTime(),
+ reverse, _pv[i->which][pi].size());
+
+ _pv[i->which][pi].appendPortionTo(result.back(), ival, prev->p, i->p);
+
+ // count both vertices as processed
+ n_processed += 2;
+ if (prev->_proc_hook.is_linked()) {
+ _ulist.erase(_ulist.iterator_to(*prev));
+ }
+ if (i->_proc_hook.is_linked()) {
+ _ulist.erase(_ulist.iterator_to(*i));
+ }
+
+ // switch to the other path
+ i = _getNeighbor(i);
+ w = i->which;
+ }
+ result.back().close(true);
+ if (reverse){
+ result.back() = result.back().reversed();
+ }
+ if (result.back().empty()) {
+ // std::cerr << "Path is empty" << std::endl;
+ throw GEOM_ERR_INTERSECGRAPH;
+ }
+ }
+
+ if (n_processed != size() * 2) {
+ // std::cerr << "Processed " << n_processed << " intersections, expected " << (size() * 2) << std::endl;
+ throw GEOM_ERR_INTERSECGRAPH;
+ }
+
+ return result;
+}
+
+/** @brief Select intersection-free path components ahead of a boolean operation based on whether
+ * they should be a part of that operation's result.
+ *
+ * Every component that has intersections will be processed by _getResult().
+ * Here we take care of paths that don't have any intersections. They are either
+ * completely inside or completely outside the other path-vector.
+ *
+ * @param result – output parameter to store the selected components.
+ * @param which – which of the two operands to search for intersection-free paths.
+ * @param inside – If set to true, add paths entirely contained inside the other path-vector to
+ * the result. If set to false, add paths entirely outside of the other path-vector instead.
+ */
+void PathIntersectionGraph::_handleNonintersectingPaths(PathVector &result, unsigned which, bool inside)
+{
+ unsigned w = which;
+ unsigned ow = (w+1) % 2;
+
+ for (std::size_t i = 0; i < _pv[w].size(); ++i) {
+ // the path data vector might have been left empty if there were no intersections at all
+ bool has_path_data = !_components[w].empty();
+ // Skip if the path has intersections
+ if (has_path_data && !_components[w][i].xlist.empty()) continue;
+ bool path_inside = false;
+
+ // Use the status flag set in the constructor if available.
+ if (has_path_data && _components[w][i].status == INSIDE) {
+ path_inside = true;
+ } else if (has_path_data && _components[w][i].status == OUTSIDE) {
+ path_inside = false;
+ } else {
+ // The status flag is ambiguous: we evaluate the winding number of the initial point.
+ int wdg = _pv[ow].winding(_pv[w][i].initialPoint());
+ path_inside = wdg % 2 != 0;
+ }
+
+ if (path_inside == inside) {
+ result.push_back(_pv[w][i]);
+ }
+ }
+}
+
+/** @brief Get an iterator to the corresponding crossing on the other path-vector.
+ *
+ * @param ILIter – an iterator to a list of intersections in one of the path-vectors.
+ * @return An iterator to the corresponding intersection in the other path-vector.
+ */
+PathIntersectionGraph::ILIter PathIntersectionGraph::_getNeighbor(ILIter iter)
+{
+ unsigned ow = (iter->which + 1) % 2;
+ return _components[ow][iter->neighbor->pos.path_index].xlist.iterator_to(*iter->neighbor);
+}
+
+/** Get the path data for the path containing the intersection given an iterator to the intersection */
+PathIntersectionGraph::PathData &
+PathIntersectionGraph::_getPathData(ILIter iter)
+{
+ return _components[iter->which][iter->pos.path_index];
+}
+
+/** Format the PathIntersectionGraph for output. */
+std::ostream &operator<<(std::ostream &os, PathIntersectionGraph const &pig)
+{
+ os << "Intersection graph:\n"
+ << pig._xs.size()/2 << " total intersections\n"
+ << pig.size() << " considered intersections\n";
+ for (std::size_t i = 0; i < pig._components[0].size(); ++i) {
+ PathIntersectionGraph::IntersectionList const &xl = pig._components[0][i].xlist;
+ for (const auto & j : xl) {
+ os << j.pos << " - " << j.neighbor->pos << " @ " << j.p << "\n";
+ }
+ }
+ return os;
+}
+
+} // namespace Geom
+
+/*
+ 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 :
+