summaryrefslogtreecommitdiffstats
path: root/tests/self-intersections-test.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/self-intersections-test.cpp')
-rw-r--r--tests/self-intersections-test.cpp219
1 files changed, 219 insertions, 0 deletions
diff --git a/tests/self-intersections-test.cpp b/tests/self-intersections-test.cpp
new file mode 100644
index 0000000..268273f
--- /dev/null
+++ b/tests/self-intersections-test.cpp
@@ -0,0 +1,219 @@
+/** @file
+ * @brief Unit tests for PathVector::intersectSelf()
+ */
+/*
+ * Authors:
+ * Rafał Siejakowski <rs@rs-math.net>
+ *
+ * Copyright 2022 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 <gtest/gtest.h>
+#include <2geom/pathvector.h>
+#include <2geom/svg-path-parser.h>
+
+using namespace Geom;
+
+#define PV(d) (parse_svg_path(d))
+#define PTH(d) (PV(d)[0])
+
+class PVSelfIntersections : public testing::Test
+{
+protected:
+ PathVector const _rectangle, _bowtie, _bowtie_curved, _bowtie_node, _openpath,
+ _open_closed_nonintersecting, _open_closed_intersecting, _tangential, _degenerate_segments,
+ _degenerate_closing, _degenerate_multiple;
+
+ PVSelfIntersections()
+ // A simple rectangle.
+ : _rectangle{PV("M 0,0 L 5,0 5,8 0,8 Z")}
+ // A polyline path with a self-intersection @(2,1).
+ , _bowtie{PV("M 0,0 L 4,2 V 0 L 0,2 Z")}
+ // A curved bow-tie path with a self-intersection @(10,5) between cubic Béziers.
+ , _bowtie_curved{PV("M 0,0 V 10 C 10,10 10,0 20,0 V 10 C 10,10 10,0 0,0 Z")}
+ // As above, but twice as large and the self-intersection @(20,10) happens at a node.
+ , _bowtie_node{PV("M 0,0 V 20 C 0,20 10,20 20,10 25,5 30,0 40,0 V 20 "
+ "C 30,20 25,15 20,10 10,0 0,0 0,0 Z")}
+ // An open path with no self-intersections ◠―◡
+ , _openpath{PV("M 0,0 A 10,10 0,0,1 20,0 L 40,0 Q 50,10 60,0")}
+ // A line and a square with no intersections | □
+ , _open_closed_nonintersecting{PV("M 0,0 V 20 M 10,0 V 20 H 30 V 0 Z")}
+ // A line slicing through a square; two self-intersections ⎅
+ , _open_closed_intersecting{PV("M 10,0 V 40 M 0,10 V 30 H 20 V 10 Z")}
+ // A circle whose diameter precisely coincides with the top side of a rectangle.
+ , _tangential{PV("M 0,0 A 10,10 0,0,1 20,0 A 10,10, 0,0,1 0,0 Z M 0,0 H 20 V 30 H 0 Z")}
+ // A rectangle containing degenerate segments.
+ , _degenerate_segments{PV("M 0,0 H 5 V 4 L 5,4 V 8 H 5 L 5,8 H 0 Z")}
+ // A rectangle with a degenerate closing segment.
+ , _degenerate_closing{PV("M 0,0 H 5 V 8 H 0 L 0,0 Z")}
+ // Multiple consecutive degenerate segments, with a degenerate closing segment in the middle.
+ , _degenerate_multiple{PV("M 0,0 L 0,0 V 0 H 0 L 5,0 V 8 H 0 L 0,0 V 0 H 0 Z")}
+ {
+ }
+};
+
+/* Ensure that no spurious intersections are returned. */
+TEST_F(PVSelfIntersections, NoSpurious)
+{
+ auto empty = PathVector();
+ EXPECT_EQ(empty.intersectSelf().size(), 0u);
+
+ auto r = _rectangle.intersectSelf();
+ EXPECT_EQ(r.size(), 0u);
+
+ auto o = _openpath.intersectSelf();
+ EXPECT_EQ(o.size(), 0u);
+
+ auto n = _open_closed_nonintersecting.intersectSelf();
+ EXPECT_EQ(n.size(), 0u);
+
+ auto d = _degenerate_segments.intersectSelf();
+ EXPECT_EQ(d.size(), 0u);
+
+ auto dc = _degenerate_closing.intersectSelf();
+ EXPECT_EQ(dc.size(), 0u);
+
+ auto dm = _degenerate_multiple.intersectSelf();
+ EXPECT_EQ(dm.size(), 0u);
+
+ auto cusp_node = PTH("M 1 3 C 12 8 42 101 86 133 C 78 168 136 83 80 64");
+ EXPECT_EQ(cusp_node.intersectSelf().size(), 0u);
+}
+
+/* Test figure-eight shaped paths */
+TEST_F(PVSelfIntersections, Bowties)
+{
+ // Simple triangular bowtie: intersection between straight lines
+ auto triangular = _bowtie.intersectSelf();
+ EXPECT_EQ(triangular.size(), 1u);
+ ASSERT_GT(triangular.size(), 0u); // To ensure access to [0]
+ EXPECT_TRUE(are_near(triangular[0].point(), Point(2, 1)));
+
+ // Curved bowtie: intersection between cubic Bézier curves
+ auto curved_intersections = _bowtie_curved.intersectSelf();
+ EXPECT_EQ(curved_intersections.size(), 1u);
+ ASSERT_GT(curved_intersections.size(), 0u);
+ EXPECT_TRUE(are_near(curved_intersections[0].point(), Point(10, 5)));
+
+ // Curved bowtie but the intersection point is a node on both paths
+ auto node_case_intersections = _bowtie_node.intersectSelf();
+ EXPECT_EQ(node_case_intersections.size(), 1u);
+ ASSERT_GT(node_case_intersections.size(), 0u);
+ EXPECT_TRUE(are_near(node_case_intersections[0].point(), Point(20, 10)));
+}
+
+/* Test intersecting an open path with a closed one */
+TEST_F(PVSelfIntersections, OpenClosed)
+{
+ // Square cut by a vertical line
+ auto open_closed = _open_closed_intersecting.intersectSelf();
+ auto const P1 = Point(10, 10);
+ auto const P2 = Point(10, 30);
+
+ ASSERT_EQ(open_closed.size(), 2u); // Prevent crash on out-of-bounds access
+ // This test doesn't care about how the intersections are ordered.
+ bool points_as_expected = (are_near(open_closed[0].point(), P1) && are_near(open_closed[1].point(), P2))
+ || (are_near(open_closed[0].point(), P2) && are_near(open_closed[1].point(), P1));
+ EXPECT_TRUE(points_as_expected);
+}
+
+/* Test some nasty, tangential crossings: a circle with a rectangle built on its diameter. */
+TEST_F(PVSelfIntersections, Tangential)
+{
+ auto circle_x_rect = _tangential.intersectSelf();
+ auto const P1 = Point(0, 0);
+ auto const P2 = Point(20, 0);
+
+ ASSERT_EQ(circle_x_rect.size(), 2u); // Prevent crash on out-of-bounds access
+ // This test doesn't care how the intersections are ordered.
+ bool points_as_expected = (are_near(circle_x_rect[0].point(), P1) && are_near(circle_x_rect[1].point(), P2))
+ || (are_near(circle_x_rect[0].point(), P2) && are_near(circle_x_rect[1].point(), P1));
+ EXPECT_TRUE(points_as_expected);
+}
+
+/* Regression test for issue https://gitlab.com/inkscape/lib2geom/-/issues/33 */
+TEST_F(PVSelfIntersections, Regression33)
+{
+ // Test case provided by Pascal Bies in the issue description.
+ auto const line = LineSegment(Point(486, 597), Point(313, 285));
+ Point const c{580.1377046525328, 325.5830744834947};
+ Point const d{289.35338528516013, 450.62476639303753};
+ auto const curve = CubicBezier(c, c, d, d);
+
+ EXPECT_EQ(curve.intersect(line).size(), 1);
+}
+
+/* Regression test for issue https://gitlab.com/inkscape/lib2geom/-/issues/46 */
+TEST_F(PVSelfIntersections, NumericalInstability)
+{
+ // Test examples provided by M.B. Fraga in the issue report.
+ auto missing_intersection = PTH("M 138 237 C 293 207 129 12 167 106 Q 205 200 309 198 z");
+ auto missing_xings = missing_intersection.intersectSelf();
+ EXPECT_EQ(missing_xings.size(), 2);
+
+ auto duplicate_intersection = PTH("M 60 280 C 60 213 236 227 158 178 S 174 306 127 310 Q 80 314 60 280 z");
+ auto const only_expected = Point(130.9693916417836, 224.587385497877);
+ auto duplicate_xings = duplicate_intersection.intersectSelf();
+ ASSERT_EQ(duplicate_xings.size(), 1);
+ EXPECT_TRUE(are_near(duplicate_xings[0].point(), only_expected));
+}
+
+/* Check various numerically challenging paths consisting of 2 cubic Béziers. */
+TEST_F(PVSelfIntersections, NumericallyChallenging)
+{
+ auto two_kinks = PTH("M 85 88 C 4 425 19 6 72 426 C 128 6 122 456 68 96");
+ EXPECT_EQ(two_kinks.intersectSelf().size(), 3);
+
+ auto omega = PTH("M 47 132 C 179 343 0 78 106 74 C 187 74 0 358 174 106");
+ EXPECT_EQ(omega.intersectSelf().size(), 0);
+
+ auto spider = PTH("M 47 132 C 203 339 0 78 106 74 C 187 74 0 358 174 106");
+ EXPECT_EQ(spider.intersectSelf().size(), 4);
+
+ auto egret = PTH("M 38 340 C 183 141 16 76 255 311 C 10 79 116 228 261 398");
+ EXPECT_EQ(egret.intersectSelf().size(), 0);
+}
+
+/* Test a regression from 88040ea2aeab8ccec2b0e96c7bda2fc7d500d5ec */
+TEST_F(PVSelfIntersections, BigonFiltering)
+{
+ auto const lens = PTH("M 0,0 C 2,1 3,1 5,0 A 2.5,1 0 1 0 0,0 Z");
+ auto const xings = lens.intersectSelf();
+ // This is a simple closed path, so we expect that no self-intersections are reported.
+ EXPECT_EQ(xings.size(), 0);
+}
+
+
+/*
+ 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 :