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 /tests | |
parent | Initial commit. (diff) | |
download | lib2geom-61f3ab8f23f4c924d455757bf3e65f8487521b5a.tar.xz lib2geom-61f3ab8f23f4c924d455757bf3e65f8487521b5a.zip |
Adding upstream version 1.3.upstream/1.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
45 files changed, 9961 insertions, 0 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..95e10d4 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,56 @@ + +find_package(GTest REQUIRED MODULE) +# Use this variable for tests which provide their own main(). +SET(2GEOM_TESTS_SRC +#bezier-utils-test +#lin_alg_test +sbasis-text-test +root-find-test +implicitization-test +#timing-test +#rtree-performance-test +) + +# Use this variable for GTest tests which should have a default main(). +SET(2GEOM_GTESTS_SRC +affine-test +angle-test +bezier-test +choose-test +circle-test +convex-hull-test +coord-test +ellipse-test +elliptical-arc-test +intersection-graph-test +interval-test +line-test +nl-vector-test +parallelogram-test +path-test +planar-graph-test +point-test +polynomial-test +rect-test +sbasis-test +self-intersections-test +) + +foreach(source ${2GEOM_GTESTS_SRC}) + add_executable(${source} ${source}.cpp) + target_include_directories(${source} PRIVATE ${GSL_INCLUDE_DIRS} ${GTK3_INCLUDE_DIRS}) + target_link_libraries(${source} 2geom GTest::Main ${GSL_LIBRARIES} ${GTK3_LIBRARIES}) + add_test(NAME ${source} COMMAND ${source}) +endforeach() + +foreach(source ${2GEOM_TESTS_SRC}) + add_executable(${source} ${source}.cpp) + target_include_directories(${source} PRIVATE ${GSL_INCLUDE_DIRS} ${GTK3_INCLUDE_DIRS}) + target_link_libraries(${source} 2geom GTest::GTest ${GSL_LIBRARIES} ${GTK3_LIBRARIES}) + add_test(NAME ${source} COMMAND ${source}) +endforeach(source) + +if(WIN32 AND 2GEOM_BUILD_SHARED) + add_custom_target(copy ALL COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_BINARY_DIR}/src/2geom/lib2geom.dll ${CMAKE_BINARY_DIR}/src/tests/lib2geom.dll) + add_dependencies(copy 2geom) +endif() diff --git a/tests/WontSnapToSomeCurveSegments.svg b/tests/WontSnapToSomeCurveSegments.svg new file mode 100644 index 0000000..b7e8dfb --- /dev/null +++ b/tests/WontSnapToSomeCurveSegments.svg @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="800" + height="400" + id="svg3975" + version="1.1" + inkscape:version="0.48+devel r10639 custom" + sodipodi:docname="825840-geom-bbox-nan-stroke-width-2.svg"> + <defs + id="defs3977"> + <marker + inkscape:stockid="Arrow2Lend" + orient="auto" + refY="0" + refX="0" + id="Arrow2Lend" + style="overflow:visible"> + <path + id="path3915" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round" + d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" + transform="matrix(-1.1,0,0,-1.1,-1.1,0)" + inkscape:connector-curvature="0" /> + </marker> + <marker + inkscape:stockid="Arrow2Mend" + orient="auto" + refY="0" + refX="0" + id="Arrow2Mend" + style="overflow:visible"> + <path + id="path3921" + style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round" + d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" + transform="scale(-0.6,-0.6)" + inkscape:connector-curvature="0" /> + </marker> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:zoom="0.76013979" + inkscape:cx="406.66823" + inkscape:cy="300.66798" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:window-width="1047" + inkscape:window-height="815" + inkscape:window-x="736" + inkscape:window-y="62" + inkscape:window-maximized="0" + borderlayer="false" + inkscape:showpageshadow="false" + inkscape:snap-center="false" + inkscape:snap-text-baseline="false" + inkscape:object-nodes="false" + inkscape:snap-midpoints="false" + inkscape:object-paths="true" + inkscape:snap-global="true" + showguides="true" + inkscape:guide-bbox="true" + inkscape:snap-intersection-paths="false" + inkscape:snap-others="false" + inkscape:snap-bbox="false" + inkscape:bbox-paths="false" + inkscape:bbox-nodes="false" + inkscape:snap-bbox-edge-midpoints="false" + inkscape:snap-bbox-midpoints="false" + inkscape:snap-smooth-nodes="false" + inkscape:snap-object-midpoints="false" + inkscape:snap-page="false"> + <inkscape:grid + type="xygrid" + id="grid4495" + empspacing="2" + visible="true" + enabled="true" + snapvisiblegridlinesonly="true" + dotted="false" + color="#009862" + opacity="0.40392157" + empcolor="#ff0000" + empopacity="0.1254902" + spacingx="50px" + spacingy="50px" /> + </sodipodi:namedview> + <metadata + id="metadata3980"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 262.6037,35.824151 c 0,0 -92.64892,-187.405851 30,-149.999981 104.06976,31.739531 170,109.9999815 170,109.9999815 l -10,-59.9999905 c 0,0 40,79.99999 -40,79.99999 -80,0 -70,-129.999981 -70,-129.999981 l 50,0 C 435.13571,-131.5667 652.76275,126.44872 505.74322,108.05672 358.73876,89.666591 292.6037,-14.175849 292.6037,15.824151 c 0,30 -30,20 -30,20 z" + id="path3132" + inkscape:connector-curvature="0" + sodipodi:nodetypes="csccsccssc" /> + </g> + <g + inkscape:groupmode="layer" + id="layer2" + inkscape:label="illustrate" /> +</svg> diff --git a/tests/affine-test.cpp b/tests/affine-test.cpp new file mode 100644 index 0000000..2ddeb1d --- /dev/null +++ b/tests/affine-test.cpp @@ -0,0 +1,429 @@ +/** @file + * @brief Unit tests for Affine + * Uses the Google Testing Framework + *//* + * Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright 2010 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/affine.h> +#include <2geom/transforms.h> + +namespace Geom { + +TEST(AffineTest, Equality) { + Affine e; // identity + Affine a(1, 2, 3, 4, 5, 6); + EXPECT_EQ(e, e); + EXPECT_EQ(e, Geom::identity()); + EXPECT_EQ(e, Geom::Affine::identity()); + EXPECT_NE(e, a); +} + +TEST(AffineTest, Classification) { + { + Affine a; // identity + EXPECT_TRUE(a.isIdentity()); + EXPECT_TRUE(a.isTranslation()); + EXPECT_TRUE(a.isScale()); + EXPECT_TRUE(a.isUniformScale()); + EXPECT_TRUE(a.isRotation()); + EXPECT_TRUE(a.isHShear()); + EXPECT_TRUE(a.isVShear()); + EXPECT_TRUE(a.isZoom()); + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_FALSE(a.isNonzeroScale()); + EXPECT_FALSE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_TRUE(a.preservesArea()); + EXPECT_TRUE(a.preservesAngles()); + EXPECT_TRUE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_FALSE(a.isSingular()); + } + { + Affine a = Translate(10, 15); // pure translation + EXPECT_FALSE(a.isIdentity()); + EXPECT_TRUE(a.isTranslation()); + EXPECT_FALSE(a.isScale()); + EXPECT_FALSE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_TRUE(a.isZoom()); + EXPECT_TRUE(a.isNonzeroTranslation()); + EXPECT_FALSE(a.isNonzeroScale()); + EXPECT_FALSE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_TRUE(a.preservesArea()); + EXPECT_TRUE(a.preservesAngles()); + EXPECT_TRUE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_FALSE(a.isSingular()); + } + { + Affine a = Scale(-1.0, 1.0); // flip on the X axis + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_TRUE(a.isScale()); + EXPECT_TRUE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_FALSE(a.isZoom()); // zoom must be non-flipping + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_TRUE(a.isNonzeroScale()); + EXPECT_TRUE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_TRUE(a.preservesArea()); + EXPECT_TRUE(a.preservesAngles()); + EXPECT_TRUE(a.preservesDistances()); + EXPECT_TRUE(a.flips()); + EXPECT_FALSE(a.isSingular()); + } + { + Affine a = Scale(0.5, 0.5); // pure uniform scale + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_TRUE(a.isScale()); + EXPECT_TRUE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_TRUE(a.isZoom()); + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_TRUE(a.isNonzeroScale()); + EXPECT_TRUE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_FALSE(a.preservesArea()); + EXPECT_TRUE(a.preservesAngles()); + EXPECT_FALSE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_FALSE(a.isSingular()); + } + { + Affine a = Scale(0.5, -0.5); // pure uniform flipping scale + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_TRUE(a.isScale()); + EXPECT_TRUE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_FALSE(a.isZoom()); // zoom must be non-flipping + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_TRUE(a.isNonzeroScale()); + EXPECT_TRUE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_FALSE(a.preservesArea()); + EXPECT_TRUE(a.preservesAngles()); + EXPECT_FALSE(a.preservesDistances()); + EXPECT_TRUE(a.flips()); + EXPECT_FALSE(a.isSingular()); + } + { + Affine a = Scale(0.5, 0.7); // pure non-uniform scale + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_TRUE(a.isScale()); + EXPECT_FALSE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_FALSE(a.isZoom()); + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_TRUE(a.isNonzeroScale()); + EXPECT_FALSE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_FALSE(a.preservesArea()); + EXPECT_FALSE(a.preservesAngles()); + EXPECT_FALSE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_FALSE(a.isSingular()); + } + { + Affine a = Scale(0.5, 2.0); // "squeeze" transform (non-uniform scale with det=1) + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_TRUE(a.isScale()); + EXPECT_FALSE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_FALSE(a.isZoom()); + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_TRUE(a.isNonzeroScale()); + EXPECT_FALSE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_TRUE(a.preservesArea()); + EXPECT_FALSE(a.preservesAngles()); + EXPECT_FALSE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_FALSE(a.isSingular()); + } + { + Affine a = Rotate(0.7); // pure rotation + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_FALSE(a.isScale()); + EXPECT_FALSE(a.isUniformScale()); + EXPECT_TRUE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_FALSE(a.isZoom()); + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_FALSE(a.isNonzeroScale()); + EXPECT_FALSE(a.isNonzeroUniformScale()); + EXPECT_TRUE(a.isNonzeroRotation()); + EXPECT_TRUE(a.isNonzeroNonpureRotation()); + EXPECT_EQ(a.rotationCenter(), Point(0.0,0.0)); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_TRUE(a.preservesArea()); + EXPECT_TRUE(a.preservesAngles()); + EXPECT_TRUE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_FALSE(a.isSingular()); + } + { + Point rotation_center(1.23,4.56); + Affine a = Translate(-rotation_center) * Rotate(0.7) * Translate(rotation_center); // rotation around (1.23,4.56) + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_FALSE(a.isScale()); + EXPECT_FALSE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_FALSE(a.isZoom()); + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_FALSE(a.isNonzeroScale()); + EXPECT_FALSE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_TRUE(a.isNonzeroNonpureRotation()); + EXPECT_TRUE(are_near(a.rotationCenter(), rotation_center, 1e-7)); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_TRUE(a.preservesArea()); + EXPECT_TRUE(a.preservesAngles()); + EXPECT_TRUE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_FALSE(a.isSingular()); + } + { + Affine a = HShear(0.5); // pure horizontal shear + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_FALSE(a.isScale()); + EXPECT_FALSE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_TRUE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_FALSE(a.isZoom()); + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_FALSE(a.isNonzeroScale()); + EXPECT_FALSE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_TRUE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_TRUE(a.preservesArea()); + EXPECT_FALSE(a.preservesAngles()); + EXPECT_FALSE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_FALSE(a.isSingular()); + } + { + Affine a = VShear(0.5); // pure vertical shear + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_FALSE(a.isScale()); + EXPECT_FALSE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_TRUE(a.isVShear()); + EXPECT_FALSE(a.isZoom()); + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_FALSE(a.isNonzeroScale()); + EXPECT_FALSE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_TRUE(a.isNonzeroVShear()); + EXPECT_TRUE(a.preservesArea()); + EXPECT_FALSE(a.preservesAngles()); + EXPECT_FALSE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_FALSE(a.isSingular()); + } + { + Affine a = Zoom(3.0, Translate(10, 15)); // zoom + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_FALSE(a.isScale()); + EXPECT_FALSE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_TRUE(a.isZoom()); + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_FALSE(a.isNonzeroScale()); + EXPECT_FALSE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_FALSE(a.preservesArea()); + EXPECT_TRUE(a.preservesAngles()); + EXPECT_FALSE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_FALSE(a.isSingular()); + + EXPECT_TRUE(a.withoutTranslation().isUniformScale()); + EXPECT_TRUE(a.withoutTranslation().isNonzeroUniformScale()); + } + { + Affine a(0, 0, 0, 0, 0, 0); // zero matrix (singular) + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_FALSE(a.isScale()); + EXPECT_FALSE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_FALSE(a.isZoom()); + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_FALSE(a.isNonzeroScale()); + EXPECT_FALSE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_FALSE(a.preservesArea()); + EXPECT_FALSE(a.preservesAngles()); + EXPECT_FALSE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_TRUE(a.isSingular()); + } + { + Affine a(0, 1, 0, 1, 10, 10); // another singular matrix + EXPECT_FALSE(a.isIdentity()); + EXPECT_FALSE(a.isTranslation()); + EXPECT_FALSE(a.isScale()); + EXPECT_FALSE(a.isUniformScale()); + EXPECT_FALSE(a.isRotation()); + EXPECT_FALSE(a.isHShear()); + EXPECT_FALSE(a.isVShear()); + EXPECT_FALSE(a.isZoom()); + EXPECT_FALSE(a.isNonzeroTranslation()); + EXPECT_FALSE(a.isNonzeroScale()); + EXPECT_FALSE(a.isNonzeroUniformScale()); + EXPECT_FALSE(a.isNonzeroRotation()); + EXPECT_FALSE(a.isNonzeroNonpureRotation()); + EXPECT_FALSE(a.isNonzeroHShear()); + EXPECT_FALSE(a.isNonzeroVShear()); + EXPECT_FALSE(a.preservesArea()); + EXPECT_FALSE(a.preservesAngles()); + EXPECT_FALSE(a.preservesDistances()); + EXPECT_FALSE(a.flips()); + EXPECT_TRUE(a.isSingular()); + } +} + +TEST(AffineTest, Inversion) { + Affine i(1, 2, 1, -2, 10, 15); // invertible + Affine n(1, 2, 1, 2, 15, 30); // non-invertible + Affine e; // identity + EXPECT_EQ(i * i.inverse(), e); + EXPECT_EQ(i.inverse().inverse(), i); + EXPECT_EQ(n.inverse(), e); + EXPECT_EQ(e.inverse(), e); +} + +TEST(AffineTest, CoordinateAccess) { + Affine a(0, 1, 2, 3, 4, 5); + for (int i=0; i<6; ++i) { + EXPECT_EQ(a[i], i); + } + for (int i=0; i<6; ++i) { + a[i] = 5*i; + } + for (int i=0; i<6; ++i) { + EXPECT_EQ(a[i], 5*i); + } +} + +TEST(AffineTest, Nearness) { + Affine a1(1, 0, 1, 2, 1e-8, 1e-8); + Affine a2(1+1e-8, 0, 1, 2-1e-8, -1e-8, -1e-8); + EXPECT_TRUE(are_near(a1, a2, 1e-7)); + EXPECT_FALSE(are_near(a1, a2, 1e-9)); +} + +TEST(AffineTest, Multiplication) { + // test whether noncommutative multiplications work correctly + Affine a1 = Scale(0.1), a2 = Translate(10, 10), a3 = Scale(10.0); + Affine t1 = Translate(1, 1), t100 = Translate(100, 100); + EXPECT_EQ(a1 * a2 * a3, t100); + EXPECT_EQ(a3 * a2 * a1, t1); +} + +} // end 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 : diff --git a/tests/angle-test.cpp b/tests/angle-test.cpp new file mode 100644 index 0000000..687be65 --- /dev/null +++ b/tests/angle-test.cpp @@ -0,0 +1,209 @@ +/** @file + * @brief Unit tests for Angle and AngleInterval. + * Uses the Google Testing Framework + *//* + * 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/angle.h> +#include <glib.h> +#include "testing.h" + +using namespace Geom; + +TEST(AngleIntervalTest, InnerAngleConstrutor) { + std::vector<AngleInterval> ivs; + + ivs.emplace_back(0, M_PI, true); + ivs.emplace_back(0, M_PI, false); + ivs.emplace_back(M_PI, 0, true); + ivs.emplace_back(M_PI, 0, false); + ivs.emplace_back(Angle(0), Angle(0), Angle(M_PI)); + + for (auto & iv : ivs) { + AngleInterval inner(iv.angleAt(0), iv.angleAt(0.5), iv.angleAt(1)); + EXPECT_EQ(inner, iv); + } +} + +TEST(AngleIntervalTest, Containment) { + AngleInterval a(0, M_PI, true); + AngleInterval b(0, M_PI, false); + AngleInterval c(M_PI, 0, true); + AngleInterval d(M_PI, 0, false); + AngleInterval e = AngleInterval::create_full(M_PI, true); + + EXPECT_TRUE(a.contains(1.)); + EXPECT_FALSE(a.contains(5.)); + EXPECT_EQ(a.extent(), M_PI); + + EXPECT_FALSE(b.contains(1.)); + EXPECT_TRUE(b.contains(5.)); + EXPECT_EQ(b.extent(), M_PI); + + EXPECT_FALSE(c.contains(1.)); + EXPECT_TRUE(c.contains(5.)); + EXPECT_EQ(c.extent(), M_PI); + + EXPECT_TRUE(d.contains(1.)); + EXPECT_FALSE(d.contains(5.)); + EXPECT_EQ(d.extent(), M_PI); + + EXPECT_TRUE(e.contains(1.)); + EXPECT_TRUE(e.contains(5.)); + EXPECT_EQ(e.extent(), 2*M_PI); +} + +TEST(AngleIntervalTest, TimeAtAngle) { + Coord pi32 = (3./2.)*M_PI; + AngleInterval a(M_PI, pi32, true); + AngleInterval b(pi32, M_PI, true); + AngleInterval c(M_PI, 0, false); + AngleInterval d(M_PI/2, M_PI, false); + AngleInterval e = AngleInterval::create_full(M_PI, true); + AngleInterval f = AngleInterval::create_full(M_PI, false); + Interval unit(0, 1); + + EXPECT_EQ(a.timeAtAngle(M_PI), 0); + EXPECT_EQ(a.timeAtAngle(pi32), 1); + EXPECT_EQ(a.extent(), M_PI/2); + for (Coord t = -1; t <= 2; t += 0.125) { + Coord angle = lerp(t, M_PI, pi32); + Coord ti = a.timeAtAngle(angle); + EXPECT_EQ(unit.contains(ti), a.contains(angle)); + EXPECT_FLOAT_EQ(ti, t); + } + + EXPECT_EQ(b.timeAtAngle(pi32), 0); + EXPECT_EQ(b.timeAtAngle(M_PI), 1); + EXPECT_EQ(b.extent(), pi32); + EXPECT_FLOAT_EQ(b.timeAtAngle(M_PI/4), 0.5); + EXPECT_FLOAT_EQ(b.timeAtAngle(0), 1./3.); + EXPECT_FLOAT_EQ(b.timeAtAngle((11./8)*M_PI), -1./12); + for (Coord t = -0.125; t <= 1.125; t += 0.0625) { + Coord angle = lerp(t, pi32, 3*M_PI); + Coord ti = b.timeAtAngle(angle); + EXPECT_EQ(unit.contains(ti), b.contains(angle)); + EXPECT_FLOAT_EQ(ti, t); + } + + EXPECT_EQ(c.timeAtAngle(M_PI), 0); + EXPECT_EQ(c.timeAtAngle(0), 1); + EXPECT_EQ(c.extent(), M_PI); + EXPECT_FLOAT_EQ(c.timeAtAngle(M_PI/2), 0.5); + for (Coord t = -0.25; t <= 1.25; t += 0.125) { + Coord angle = lerp(t, M_PI, 0); + Coord ti = c.timeAtAngle(angle); + EXPECT_EQ(unit.contains(ti), c.contains(angle)); + EXPECT_FLOAT_EQ(ti, t); + } + + EXPECT_EQ(d.timeAtAngle(M_PI/2), 0); + EXPECT_EQ(d.timeAtAngle(M_PI), 1); + EXPECT_EQ(d.extent(), pi32); + EXPECT_FLOAT_EQ(d.timeAtAngle(-M_PI/4), 0.5); + for (Coord t = -0.125; t <= 1.125; t += 0.0625) { + Coord angle = lerp(t, M_PI/2, -M_PI); + Coord ti = d.timeAtAngle(angle); + EXPECT_EQ(unit.contains(ti), d.contains(angle)); + EXPECT_FLOAT_EQ(ti, t); + } + + EXPECT_EQ(e.timeAtAngle(M_PI), 0); + EXPECT_EQ(e.extent(), 2*M_PI); + EXPECT_FLOAT_EQ(e.timeAtAngle(0), 0.5); + for (Coord t = 0; t < 1; t += 0.125) { + Coord angle = lerp(t, M_PI, 3*M_PI); + Coord ti = e.timeAtAngle(angle); + EXPECT_EQ(unit.contains(ti), true); + EXPECT_EQ(e.contains(angle), true); + EXPECT_FLOAT_EQ(ti, t); + } + + EXPECT_EQ(f.timeAtAngle(M_PI), 0); + EXPECT_EQ(f.extent(), 2*M_PI); + EXPECT_FLOAT_EQ(e.timeAtAngle(0), 0.5); + for (Coord t = 0; t < 1; t += 0.125) { + Coord angle = lerp(t, M_PI, -M_PI); + Coord ti = f.timeAtAngle(angle); + EXPECT_EQ(unit.contains(ti), true); + EXPECT_EQ(f.contains(angle), true); + EXPECT_FLOAT_EQ(ti, t); + } +} + +TEST(AngleIntervalTest, AngleAt) { + Coord pi32 = (3./2.)*M_PI; + AngleInterval a(M_PI, pi32, true); + AngleInterval c(M_PI, 0, false); + AngleInterval f1 = AngleInterval::create_full(0, true); + AngleInterval f2 = AngleInterval::create_full(M_PI, false); + + EXPECT_EQ(a.angleAt(0), M_PI); + EXPECT_EQ(a.angleAt(1), pi32); + EXPECT_EQ(a.extent(), M_PI/2); + for (Coord t = -1; t <= 2; t += 0.125) { + EXPECT_FLOAT_EQ(a.angleAt(t), Angle(lerp(t, M_PI, pi32))); + } + + EXPECT_EQ(c.angleAt(0), M_PI); + EXPECT_EQ(c.angleAt(1), 0.); + EXPECT_EQ(c.extent(), M_PI); + for (Coord t = -0.25; t <= 1.25; t += 0.0625) { + EXPECT_FLOAT_EQ(c.angleAt(t), Angle(lerp(t, M_PI, 0))); + } + + EXPECT_EQ(f1.angleAt(0), 0.); + EXPECT_EQ(f1.angleAt(1), 0.); + for (Coord t = 0; t < 1; t += 0.125) { + EXPECT_FLOAT_EQ(f1.angleAt(t), Angle(lerp(t, 0, 2*M_PI))); + } + EXPECT_EQ(f2.angleAt(0), M_PI); + EXPECT_EQ(f2.angleAt(1), M_PI); + for (Coord t = 0; t < 1; t += 0.125) { + EXPECT_FLOAT_EQ(f2.angleAt(t), Angle(lerp(t, M_PI, -M_PI))); + } +} + +TEST(AngleIntervalTest, Extent) { + Coord pi32 = (3./2.)*M_PI; + AngleInterval a(M_PI, pi32, true); + AngleInterval b(pi32, M_PI, true); + AngleInterval c(M_PI, 0, false); + AngleInterval d(M_PI/2, M_PI, false); + + EXPECT_EQ(a.extent(), M_PI/2); + EXPECT_EQ(a.sweepAngle(), M_PI/2); + EXPECT_EQ(b.extent(), pi32); + EXPECT_EQ(b.sweepAngle(), pi32); + EXPECT_EQ(c.extent(), M_PI); + EXPECT_EQ(c.sweepAngle(), -M_PI); + EXPECT_EQ(d.extent(), pi32); + EXPECT_EQ(d.sweepAngle(), -pi32); +} diff --git a/tests/bezier-sbasis-transforms.py b/tests/bezier-sbasis-transforms.py new file mode 100644 index 0000000..1dc850f --- /dev/null +++ b/tests/bezier-sbasis-transforms.py @@ -0,0 +1,72 @@ +#!/usr/bin/python + +from Numeric import * +from LinearAlgebra import * + +pascals_triangle = [] +rows_done = 0 + +def choose(n, k): + r = 1 + for i in range(1,k+1): + r *= n-k+i + r /= i + return r + +# http://www.research.att.com/~njas/sequences/A109954 +def T(n, k): + return ((-1)**(n+k))*choose(n+k+2, 2*k+2) + +def inver(q): + result = zeros((q+2,q+2)) + q2 = q/2+1 + for i in range(q2): + for j in range(i+1): + val = T(i,j) + result[q/2-j][q/2-i] = val + result[q/2+j+2][q/2+i+2] = val + result[q/2+j+2][q/2-i-1] = -val + if q/2+i+3 < q+2: + result[q/2-j][q/2+i+3] = -val + + for i in range(q+2): + result[q2][i] = [1,-1][(i-q2)%2] + return result + +def simple(q): + result = zeros((q+2,q+2)) + for i in range(q/2+1): + for j in range(q+1): + result[j][i] = choose(q-2*i, j-i) + result[j+1][q-i+1] = choose(q-2*i, j-i) + result[q/2+1][q/2+1] = 1 + return result + +print "The aim of the game is to work out the correct indexing to make the two matrices match :)" + +s = simple(4) +si = floor(inverse(s)+0.5) +print si.astype(Int) +print inver(4) +exit(0) +print "<html><head><title></title></head><body>" + +def arrayhtml(a): + s = "<table>" + r,c = a.shape + for i in range(r): + s += "<tr>"; + for j in range(c): + s += "<td>%g</td>" % a[i,j] + s += "</tr>" + s += "</table>" + return s + +for i in [21]:#range(1,13,2): + s = simple(i) + print "<h1>T<sup>-1</sup> = </h1>" + print arrayhtml(s) + print "<h1>T = </h1>" + print arrayhtml(floor(inverse(s)+0.5)) + +print "</body></html>" diff --git a/tests/bezier-test.cpp b/tests/bezier-test.cpp new file mode 100644 index 0000000..0799393 --- /dev/null +++ b/tests/bezier-test.cpp @@ -0,0 +1,680 @@ +/** @file + * @brief Unit tests for Affine. + * Uses the Google Testing Framework + *//* + * Authors: + * Nathan Hurst <njh@njhurst.com> + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright 2010 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 "testing.h" +#include <iostream> + +#include <2geom/bezier.h> +#include <2geom/polynomial.h> +#include <2geom/basic-intersection.h> +#include <2geom/bezier-curve.h> +#include <vector> +#include <iterator> +#include <glib.h> + +using std::vector, std::min, std::max; +using namespace Geom; + +Poly lin_poly(double a, double b) { // ax + b + Poly p; + p.push_back(b); + p.push_back(a); + return p; +} + +bool are_equal(Bezier A, Bezier B) { + int maxSize = max(A.size(), B.size()); + double t = 0., dt = 1./maxSize; + + for(int i = 0; i <= maxSize; i++) { + EXPECT_FLOAT_EQ(A.valueAt(t), B.valueAt(t));// return false; + t += dt; + } + return true; +} + +class BezierTest : public ::testing::Test { +protected: + + BezierTest() + : zero(fragments[0]) + , unit(fragments[1]) + , hump(fragments[2]) + , wiggle(fragments[3]) + { + zero = Bezier(0.0,0.0); + unit = Bezier(0.0,1.0); + hump = Bezier(0,1,0); + wiggle = Bezier(0,1,-2,3); + } + + Bezier fragments[4]; + Bezier &zero, &unit, &hump, &wiggle; +}; + +TEST_F(BezierTest, Basics) { + + //std::cout << unit <<std::endl; + //std::cout << hump <<std::endl; + + EXPECT_TRUE(Bezier(0,0,0,0).isZero()); + EXPECT_TRUE(Bezier(0,1,2,3).isFinite()); + + EXPECT_EQ(3u, Bezier(0,2,4,5).order()); + + ///cout << " Bezier::Bezier(const Bezier& b);\n"; + //cout << Bezier(wiggle) << " == " << wiggle << endl; + + //cout << "explicit Bezier(unsigned ord);\n"; + //cout << Bezier(10) << endl; + + //cout << "Bezier(Coord c0, Coord c1);\n"; + //cout << Bezier(0.0,1.0) << endl; + + //cout << "Bezier(Coord c0, Coord c1, Coord c2);\n"; + //cout << Bezier(0,1, 2) << endl; + + //cout << "Bezier(Coord c0, Coord c1, Coord c2, Coord c3);\n"; + //cout << Bezier(0,1,2,3) << endl; + + //cout << "unsigned degree();\n"; + EXPECT_EQ(2u, hump.degree()); + + //cout << "unsigned size();\n"; + EXPECT_EQ(3u, hump.size()); +} + +TEST_F(BezierTest, ValueAt) { + EXPECT_EQ(0.0, wiggle.at0()); + EXPECT_EQ(3.0, wiggle.at1()); + + EXPECT_EQ(0.0, wiggle.valueAt(0.5)); + + EXPECT_EQ(0.0, wiggle(0.5)); + + //cout << "SBasis toSBasis();\n"; + //cout << unit.toSBasis() << endl; + //cout << hump.toSBasis() << endl; + //cout << wiggle.toSBasis() << endl; +} + +TEST_F(BezierTest, Casteljau) { + unsigned N = wiggle.order() + 1; + std::vector<Coord> left(N), right(N); + std::vector<Coord> left2(N), right2(N); + double eps = 1e-15; + + for (unsigned i = 0; i < 10000; ++i) { + double t = g_random_double_range(0, 1); + double vok = bernstein_value_at(t, &wiggle[0], wiggle.order()); + double v = casteljau_subdivision<double>(t, &wiggle[0], &left[0], &right[0], wiggle.order()); + EXPECT_near(v, vok, eps); + EXPECT_EQ(left[0], wiggle.at0()); + EXPECT_EQ(left[wiggle.order()], right[0]); + EXPECT_EQ(right[wiggle.order()], wiggle.at1()); + + double vl = casteljau_subdivision<double>(t, &wiggle[0], &left2[0], NULL, wiggle.order()); + double vr = casteljau_subdivision<double>(t, &wiggle[0], NULL, &right2[0], wiggle.order()); + EXPECT_EQ(vl, vok); + EXPECT_near(vr, vok, eps); + EXPECT_vector_near(left2, left, eps); + EXPECT_vector_equal(right2, right); + + double vnone = casteljau_subdivision<double>(t, &wiggle[0], NULL, NULL, wiggle.order()); + EXPECT_near(vnone, vok, 1e-12); + } +} + +TEST_F(BezierTest, Portion) { + constexpr Coord eps{1e-12}; + + for (unsigned i = 0; i < 10000; ++i) { + double from = g_random_double_range(0, 1); + double to = g_random_double_range(0, 1); + for (auto & input : fragments) { + Bezier result = portion(input, from, to); + + // the endpoints must correspond exactly + EXPECT_near(result.at0(), input.valueAt(from), eps); + EXPECT_near(result.at1(), input.valueAt(to), eps); + } + } +} + +TEST_F(BezierTest, Subdivide) { + std::vector<std::pair<Bezier, double> > errors; + for (unsigned i = 0; i < 10000; ++i) { + double t = g_random_double_range(0, 1e-6); + for (auto & input : fragments) { + std::pair<Bezier, Bezier> result = input.subdivide(t); + + // the endpoints must correspond exactly + // moreover, the subdivision point must be exactly equal to valueAt(t) + EXPECT_DOUBLE_EQ(result.first.at0(), input.at0()); + EXPECT_DOUBLE_EQ(result.first.at1(), result.second.at0()); + EXPECT_DOUBLE_EQ(result.second.at0(), input.valueAt(t)); + EXPECT_DOUBLE_EQ(result.second.at1(), input.at1()); + + // ditto for valueAt + EXPECT_DOUBLE_EQ(result.first.valueAt(0), input.valueAt(0)); + EXPECT_DOUBLE_EQ(result.first.valueAt(1), result.second.valueAt(0)); + EXPECT_DOUBLE_EQ(result.second.valueAt(0), input.valueAt(t)); + EXPECT_DOUBLE_EQ(result.second.valueAt(1), input.valueAt(1)); + + if (result.first.at1() != result.second.at0()) { + errors.emplace_back(input, t); + } + } + } + if (!errors.empty()) { + std::cout << "Found " << errors.size() << " subdivision errors" << std::endl; + for (unsigned i = 0; i < errors.size(); ++i) { + std::cout << "Error #" << i << ":\n" + << errors[i].first << "\n" + << "t: " << format_coord_nice(errors[i].second) << std::endl; + } + } +} + +TEST_F(BezierTest, Mutation) { +//Coord &operator[](unsigned ix); +//Coord const &operator[](unsigned ix); +//void setCoeff(unsigned ix double val); + //cout << "bigun\n"; + Bezier bigun(Bezier::Order(30)); + bigun.setCoeff(5,10.0); + for(unsigned i = 0; i < bigun.size(); i++) { + EXPECT_EQ((i == 5) ? 10 : 0, bigun[i]); + } + + bigun[5] = -3; + for(unsigned i = 0; i < bigun.size(); i++) { + EXPECT_EQ((i == 5) ? -3 : 0, bigun[i]); + } +} + +TEST_F(BezierTest, MultiDerivative) { + vector<double> vnd = wiggle.valueAndDerivatives(0.5, 5); + expect_array((const double[]){0,0,12,72,0,0}, vnd); +} + +TEST_F(BezierTest, DegreeElevation) { + EXPECT_TRUE(are_equal(wiggle, wiggle)); + Bezier Q = wiggle; + Bezier P = Q.elevate_degree(); + EXPECT_EQ(P.size(), Q.size()+1); + //EXPECT_EQ(0, P.forward_difference(1)[0]); + EXPECT_TRUE(are_equal(Q, P)); + Q = wiggle; + P = Q.elevate_to_degree(10); + EXPECT_EQ(10u, P.order()); + EXPECT_TRUE(are_equal(Q, P)); + //EXPECT_EQ(0, P.forward_difference(10)[0]); + /*Q = wiggle.elevate_degree(); + P = Q.reduce_degree(); + EXPECT_EQ(P.size()+1, Q.size()); + EXPECT_TRUE(are_equal(Q, P));*/ +} +//std::pair<Bezier, Bezier > subdivide(Coord t); + +// Constructs a linear Bezier with root at t +Bezier linear_root(double t) { + return Bezier(0-t, 1-t); +} + +// Constructs a Bezier with roots at the locations in x +Bezier array_roots(vector<double> x) { + Bezier b(1); + for(double i : x) { + b = multiply(b, linear_root(i)); + } + return b; +} + +TEST_F(BezierTest, Deflate) { + Bezier b = array_roots(vector_from_array((const double[]){0,0.25,0.5})); + EXPECT_FLOAT_EQ(0, b.at0()); + b = b.deflate(); + EXPECT_FLOAT_EQ(0, b.valueAt(0.25)); + b = b.subdivide(0.25).second; + EXPECT_FLOAT_EQ(0, b.at0()); + b = b.deflate(); + const double rootposition = (0.5-0.25) / (1-0.25); + constexpr Coord eps{1e-12}; + EXPECT_near(0.0, b.valueAt(rootposition), eps); + b = b.subdivide(rootposition).second; + EXPECT_near(0.0, b.at0(), eps); +} + +TEST_F(BezierTest, Roots) { + expect_array((const double[]){0, 0.5, 0.5}, wiggle.roots()); + + /*Bezier bigun(Bezier::Order(30)); + for(unsigned i = 0; i < bigun.size(); i++) { + bigun.setCoeff(i,rand()-0.5); + } + cout << bigun.roots() << endl;*/ + + // The results of our rootfinding are at the moment fairly inaccurate. + double eps = 5e-4; + + vector<vector<double> > tests; + tests.push_back(vector_from_array((const double[]){0})); + tests.push_back(vector_from_array((const double[]){1})); + tests.push_back(vector_from_array((const double[]){0, 0})); + tests.push_back(vector_from_array((const double[]){0.5})); + tests.push_back(vector_from_array((const double[]){0.5, 0.5})); + tests.push_back(vector_from_array((const double[]){0.1, 0.1})); + tests.push_back(vector_from_array((const double[]){0.1, 0.1, 0.1})); + tests.push_back(vector_from_array((const double[]){0.25,0.75})); + tests.push_back(vector_from_array((const double[]){0.5,0.5})); + tests.push_back(vector_from_array((const double[]){0, 0.2, 0.6, 0.6, 1})); + tests.push_back(vector_from_array((const double[]){.1,.2,.3,.4,.5,.6})); + tests.push_back(vector_from_array((const double[]){0.25,0.25,0.25,0.75,0.75,0.75})); + + for(auto & test : tests) { + Bezier b = array_roots(test); + //std::cout << tests[test_i] << ": " << b << std::endl; + //std::cout << b.roots() << std::endl; + EXPECT_vector_near(test, b.roots(), eps); + } +} + +TEST_F(BezierTest, BoundsExact) { + OptInterval unit_bounds = bounds_exact(unit); + EXPECT_EQ(unit_bounds->min(), 0); + EXPECT_EQ(unit_bounds->max(), 1); + + OptInterval hump_bounds = bounds_exact(hump); + EXPECT_EQ(hump_bounds->min(), 0); + EXPECT_FLOAT_EQ(hump_bounds->max(), hump.valueAt(0.5)); + + OptInterval wiggle_bounds = bounds_exact(wiggle); + EXPECT_EQ(wiggle_bounds->min(), 0); + EXPECT_EQ(wiggle_bounds->max(), 3); +} + +TEST_F(BezierTest, Operators) { + // Test equality operators + EXPECT_EQ(zero, zero); + EXPECT_EQ(hump, hump); + EXPECT_EQ(wiggle, wiggle); + EXPECT_EQ(unit, unit); + + EXPECT_NE(zero, hump); + EXPECT_NE(hump, zero); + EXPECT_NE(wiggle, hump); + EXPECT_NE(zero, wiggle); + EXPECT_NE(wiggle, unit); + + // Recall that hump == Bezier(0,1,0); + EXPECT_EQ(hump + 3, Bezier(3, 4, 3)); + EXPECT_EQ(hump - 3, Bezier(-3, -2, -3)); + EXPECT_EQ(hump * 3, Bezier(0, 3, 0)); + EXPECT_EQ(hump / 3, Bezier(0, 1.0/3.0, 0)); + EXPECT_EQ(-hump, Bezier(0, -1, 0)); + + Bezier reverse_wiggle = reverse(wiggle); + EXPECT_EQ(reverse_wiggle.at0(), wiggle.at1()); + EXPECT_EQ(reverse_wiggle.at1(), wiggle.at0()); + EXPECT_TRUE(are_equal(reverse(reverse_wiggle), wiggle)); + + //cout << "Bezier portion(const Bezier & a, double from, double to);\n"; + //cout << portion(Bezier(0.0,2.0), 0.5, 1) << endl; + +// std::vector<Point> bezier_points(const D2<Bezier > & a) { + + /*cout << "Bezier derivative(const Bezier & a);\n"; + std::cout << derivative(hump) <<std::endl; + std::cout << integral(hump) <<std::endl;*/ + + EXPECT_TRUE(are_equal(derivative(integral(wiggle)), wiggle)); + //std::cout << derivative(integral(hump)) <<std::endl; + expect_array((const double []){0.5}, derivative(hump).roots()); + + EXPECT_TRUE(bounds_fast(hump)->contains(Interval(0,hump.valueAt(0.5)))); + + EXPECT_EQ(Interval(0,hump.valueAt(0.5)), *bounds_exact(hump)); + + Interval tight_local_bounds(min(hump.valueAt(0.3),hump.valueAt(0.6)), + hump.valueAt(0.5)); + EXPECT_TRUE(bounds_local(hump, Interval(0.3, 0.6))->contains(tight_local_bounds)); + + Bezier Bs[] = {unit, hump, wiggle}; + for(auto B : Bs) { + Bezier product = multiply(B, B); + for(int i = 0; i <= 16; i++) { + double t = i/16.0; + double b = B.valueAt(t); + EXPECT_near(b*b, product.valueAt(t), 1e-12); + } + } +} + +struct XPt { + XPt(Coord x, Coord y, Coord ta, Coord tb) + : p(x, y), ta(ta), tb(tb) + {} + XPt() {} + Point p; + Coord ta, tb; +}; + +struct XTest { + D2<Bezier> a; + D2<Bezier> b; + std::vector<XPt> s; +}; + +struct CILess { + bool operator()(CurveIntersection const &a, CurveIntersection const &b) const { + if (a.first < b.first) return true; + if (a.first == b.first && a.second < b.second) return true; + return false; + } +}; + +TEST_F(BezierTest, Intersection) { + /* Intersection test cases taken from: + * Dieter Lasser (1988), Calculating the Self-Intersections of Bezier Curves + * https://archive.org/stream/calculatingselfi00lass + * + * The intersection points are not actually calculated to a high precision + * in the paper. The most relevant tests are whether the curves actually + * intersect at the returned time values (i.e. whether a(ta) = b(tb)) + * and whether the number of intersections is correct. + */ + typedef D2<Bezier> D2Bez; + std::vector<XTest> tests; + + // Example 1 + tests.emplace_back(); + tests.back().a = D2Bez(Bezier(-3.3, -3.3, 0, 3.3, 3.3), Bezier(1.3, -0.7, 2.3, -0.7, 1.3)); + tests.back().b = D2Bez(Bezier(-4.0, -4.0, 0, 4.0, 4.0), Bezier(-0.35, 3.0, -2.6, 3.0, -0.35)); + tests.back().s.resize(4); + tests.back().s[0] = XPt(-3.12109, 0.76362, 0.09834, 0.20604); + tests.back().s[1] = XPt(-1.67341, 0.60298, 0.32366, 0.35662); + tests.back().s[2] = XPt(1.67341, 0.60298, 0.67634, 0.64338); + tests.back().s[3] = XPt(3.12109, 0.76362, 0.90166, 0.79396); + + // Example 2 + tests.emplace_back(); + tests.back().a = D2Bez(Bezier(0, 0, 3, 3), Bezier(0, 14, -9, 5)); + tests.back().b = D2Bez(Bezier(-1, 13, -10, 4), Bezier(4, 4, 1, 1)); + tests.back().s.resize(9); + tests.back().s[0] = XPt(0.00809, 1.17249, 0.03029, 0.85430); + tests.back().s[1] = XPt(0.02596, 1.97778, 0.05471, 0.61825); + tests.back().s[2] = XPt(0.17250, 3.99191, 0.14570, 0.03029); + tests.back().s[3] = XPt(0.97778, 3.97404, 0.38175, 0.05471); + tests.back().s[4] = XPt(1.5, 2.5, 0.5, 0.5); + tests.back().s[5] = XPt(2.02221, 1.02596, 0.61825, 0.94529); + tests.back().s[6] = XPt(2.82750, 1.00809, 0.85430, 0.96971); + tests.back().s[7] = XPt(2.97404, 3.02221, 0.94529, 0.38175); + tests.back().s[8] = XPt(2.99191, 3.82750, 0.96971, 0.14570); + + // Example 3 + tests.emplace_back(); + tests.back().a = D2Bez(Bezier(-5, -5, -3, 0, 3, 5, 5), Bezier(0, 3.555, -1, 4.17, -1, 3.555, 0)); + tests.back().b = D2Bez(Bezier(-6, -6, -3, 0, 3, 6, 6), Bezier(3, -0.555, 4, -1.17, 4, -0.555, 3)); + tests.back().s.resize(6); + tests.back().s[0] = XPt(-3.64353, 1.49822, 0.23120, 0.27305); + tests.back().s[1] = XPt(-2.92393, 1.50086, 0.29330, 0.32148); + tests.back().s[2] = XPt(-0.77325, 1.49989, 0.44827, 0.45409); + tests.back().s[3] = XPt(0.77325, 1.49989, 0.55173, 0.54591); + tests.back().s[4] = XPt(2.92393, 1.50086, 0.70670, 0.67852); + tests.back().s[5] = XPt(3.64353, 1.49822, 0.76880, 0.72695); + + // Example 4 + tests.emplace_back(); + tests.back().a = D2Bez(Bezier(-4, -10, -2, -2, 2, 2, 10, 4), Bezier(0, 6, 6, 0, 0, 6, 6, 0)); + tests.back().b = D2Bez(Bezier(-8, 0, 8), Bezier(1, 6, 1)); + tests.back().s.resize(4); + tests.back().s[0] = XPt(-5.69310, 2.23393, 0.06613, 0.14418); + tests.back().s[1] = XPt(-2.68113, 3.21920, 0.35152, 0.33243); + tests.back().s[2] = XPt(2.68113, 3.21920, 0.64848, 0.66757); + tests.back().s[3] = XPt(5.69310, 2.23393, 0.93387, 0.85582); + + //std::cout << std::setprecision(5); + + for (unsigned i = 0; i < tests.size(); ++i) { + BezierCurve a(tests[i].a), b(tests[i].b); + std::vector<CurveIntersection> xs; + xs = a.intersect(b, 1e-8); + std::sort(xs.begin(), xs.end(), CILess()); + //xs.erase(std::unique(xs.begin(), xs.end(), XEqual()), xs.end()); + + std::cout << "\n\n" + << "===============================\n" + << "=== Intersection Testcase " << i+1 << " ===\n" + << "===============================\n" << std::endl; + + EXPECT_EQ(xs.size(), tests[i].s.size()); + //if (xs.size() != tests[i].s.size()) continue; + + for (unsigned j = 0; j < std::min(xs.size(), tests[i].s.size()); ++j) { + std::cout << xs[j].first << " = " << a.pointAt(xs[j].first) << " " + << xs[j].second << " = " << b.pointAt(xs[j].second) << "\n" + << tests[i].s[j].ta << " = " << tests[i].a.valueAt(tests[i].s[j].ta) << " " + << tests[i].s[j].tb << " = " << tests[i].b.valueAt(tests[i].s[j].tb) << std::endl; + } + + EXPECT_intersections_valid(a, b, xs, 1e-6); + } + + #if 0 + // these contain second-order intersections + Coord a5x[] = {-1.5, -1.5, -10, -10, 0, 10, 10, 1.5, 1.5}; + Coord a5y[] = {0, -8, -8, 9, 9, 9, -8, -8, 0}; + Coord b5x[] = {-3, -12, 0, 12, 3}; + Coord b5y[] = {-5, 8, 2.062507, 8, -5}; + Coord p5x[] = {-3.60359, -5.44653, 0, 5.44653, 3.60359}; + Coord p5y[] = {-4.10631, -0.76332, 4.14844, -0.76332, -4.10631}; + Coord p5ta[] = {0.01787, 0.10171, 0.5, 0.89829, 0.98213}; + Coord p5tb[] = {0.12443, 0.28110, 0.5, 0.71890, 0.87557}; + + Coord a6x[] = {5, 14, 10, -12, -12, -2}; + Coord a6y[] = {1, 6, -6, -6, 2, 2}; + Coord b6x[] = {0, 2, -10.5, -10.5, 3.5, 3, 8, 6}; + Coord b6y[] = {0, -8, -8, 9, 9, -4.129807, -4.129807, 3}; + Coord p6x[] = {6.29966, 5.87601, 0.04246, -4.67397, -3.57214}; + Coord p6y[] = {1.63288, -0.86192, -2.38219, -2.17973, 1.91463}; + Coord p6ta[] = {0.03184, 0.33990, 0.49353, 0.62148, 0.96618}; + Coord p6tb[] = {0.96977, 0.85797, 0.05087, 0.28232, 0.46102}; + #endif +} + +/** Basic test for intersecting a quadratic Bézier with a line segment. */ +TEST_F(BezierTest, QuadraticIntersectLineSeg) +{ + double const EPS = 1e-12; + auto const bow = QuadraticBezier({0, 0}, {1, 1}, {2, 0}); + auto const highhoriz = LineSegment(Point(0, 0), Point(2, 0)); + auto const midhoriz = LineSegment(Point(0, 0.25), Point(2, 0.25)); + auto const lowhoriz = LineSegment(Point(0, 0.5), Point(2, 0.5)); + auto const noninters = LineSegment(Point(0, 0.5 + EPS), Point(2, 0.5 + EPS)); + auto const noninters2 = LineSegment(Point(1, 0), Point(1, 0.5 - EPS)); + + auto const endpoint_intersections = bow.intersect(highhoriz, EPS); + EXPECT_EQ(endpoint_intersections.size(), 2); + EXPECT_intersections_valid(bow, highhoriz, endpoint_intersections, EPS); + for (auto const &ex : endpoint_intersections) { + EXPECT_DOUBLE_EQ(ex.point()[Y], 0.0); + } + + auto const mid_intersections = bow.intersect(midhoriz, EPS); + EXPECT_EQ(mid_intersections.size(), 2); + EXPECT_intersections_valid(bow, midhoriz, mid_intersections, EPS); + for (auto const &mx : mid_intersections) { + EXPECT_DOUBLE_EQ(mx.point()[Y], 0.25); + } + + auto const tangent_intersection = bow.intersect(lowhoriz, EPS); + EXPECT_EQ(tangent_intersection.size(), 1); + EXPECT_intersections_valid(bow, lowhoriz, tangent_intersection, EPS); + for (auto const &tx : tangent_intersection) { + EXPECT_DOUBLE_EQ(tx.point()[Y], 0.5); + } + + auto no_intersections = bow.intersect(noninters, EPS); + EXPECT_TRUE(no_intersections.empty()); + + no_intersections = bow.intersect(noninters2, EPS); + EXPECT_TRUE(no_intersections.empty()); +} + +TEST_F(BezierTest, QuadraticIntersectLineRandom) +{ + g_random_set_seed(0xB747A380); + auto const diagonal = LineSegment(Point(0, 0), Point(1, 1)); + double const EPS = 1e-12; + + for (unsigned i = 0; i < 10'000; i++) { + auto q = QuadraticBezier({0, 1}, {g_random_double_range(0.0, 1.0), g_random_double_range(0.0, 1.0)}, {1, 0}); + auto xings = q.intersect(diagonal, EPS); + ASSERT_EQ(xings.size(), 1); + auto pt = xings[0].point(); + EXPECT_TRUE(are_near(pt[X], pt[Y], EPS)); + EXPECT_intersections_valid(q, diagonal, xings, EPS); + } +} + +/** Basic test for intersecting a cubic Bézier with a line segment. */ +TEST_F(BezierTest, CubicIntersectLine) +{ + double const EPS = 1e-12; + auto const wavelet = CubicBezier({0, 0}, {1, 2}, {0, -2}, {1, 0}); + + auto const unit_seg = LineSegment(Point(0, 0), Point(1, 0)); + auto const expect3 = wavelet.intersect(unit_seg, EPS); + EXPECT_EQ(expect3.size(), 3); + EXPECT_intersections_valid(wavelet, unit_seg, expect3, EPS); + + auto const half_seg = LineSegment(Point(0, 0), Point(0.5, 0)); + auto const expect2 = wavelet.intersect(half_seg, EPS); + EXPECT_EQ(expect2.size(), 2); + EXPECT_intersections_valid(wavelet, half_seg, expect2, EPS); + + auto const less_than_half = LineSegment(Point(0, 0), Point(0.5 - EPS, 0)); + auto const expect1 = wavelet.intersect(less_than_half, EPS); + EXPECT_EQ(expect1.size(), 1); + EXPECT_intersections_valid(wavelet, less_than_half, expect1, EPS); + + auto const dollar_stroke = LineSegment(Point(0, 0.5), Point(1, -0.5)); + auto const dollar_xings = wavelet.intersect(dollar_stroke, EPS); + EXPECT_EQ(dollar_xings.size(), 3); + EXPECT_intersections_valid(wavelet, dollar_stroke, dollar_xings, EPS); +} + +TEST_F(BezierTest, CubicIntersectLineRandom) +{ + g_random_set_seed(0xCAFECAFE); + auto const diagonal = LineSegment(Point(0, 0), Point(1, 1)); + double const EPS = 1e-8; + + for (unsigned i = 0; i < 10'000; i++) { + double a1 = g_random_double_range(0.0, 1.0); + double a2 = g_random_double_range(a1, 1.0); + double b1 = g_random_double_range(0.0, 1.0); + double b2 = g_random_double_range(0.0, b1); + + auto c = CubicBezier({0, 1}, {a1, a2}, {b1, b2}, {1, 0}); + auto xings = c.intersect(diagonal, EPS); + ASSERT_EQ(xings.size(), 1); + auto pt = xings[0].point(); + EXPECT_TRUE(are_near(pt[X], pt[Y], EPS)); + EXPECT_intersections_valid(c, diagonal, xings, EPS); + } +} + +/** Regression test for issue https://gitlab.com/inkscape/lib2geom/-/issues/47 . */ +TEST_F(BezierTest, Balloon) +{ + auto const loop = CubicBezier({0, 0}, {4, -2}, {4, 2}, {0, 0}); + auto const seghoriz = LineSegment(Point(-1, 0), Point(0, 0)); + + for (double EPS : {1e-6, 1e-9, 1e-12}) { + // We expect that 2 intersections are found: one at each end of the loop, + // both at the coordinates (0, 0). + auto xings_horiz = loop.intersect(seghoriz, EPS); + EXPECT_EQ(xings_horiz.size(), 2); + EXPECT_intersections_valid(loop, seghoriz, xings_horiz, EPS); + } +} + +TEST_F(BezierTest, ExpandToTransformedTest) +{ + auto test_curve = [] (Curve const &c) { + constexpr int N = 50; + for (int i = 0; i < N; i++) { + auto angle = 2 * M_PI * i / N; + auto transform = Affine(Rotate(angle)); + + auto copy = std::unique_ptr<Curve>(c.duplicate()); + *copy *= transform; + auto box1 = copy->boundsExact(); + + auto pt = c.initialPoint() * transform; + auto box2 = Rect(pt, pt); + c.expandToTransformed(box2, transform); + + for (auto i : { X, Y }) { + EXPECT_DOUBLE_EQ(box1[i].min(), box2[i].min()); + EXPECT_DOUBLE_EQ(box1[i].max(), box2[i].max()); + } + } + }; + + test_curve(LineSegment(Point(-1, 0), Point(1, 2))); + test_curve(QuadraticBezier(Point(-1, 0), Point(1, 1), Point(3, 0))); + test_curve(CubicBezier(Point(-1, 0), Point(1, 1), Point(2, -2), Point(3, 0))); +} + +TEST_F(BezierTest, ForwardDifferenceTest) +{ + auto b = Bezier(3, 4, 2, -5, 7); + EXPECT_EQ(b.forward_difference(1), Bezier(19, 34, 22, 5)); + EXPECT_EQ(b.forward_difference(2), Bezier(-3, 2, 2)); +} + +/* + 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 : diff --git a/tests/bezier-utils-test.cpp b/tests/bezier-utils-test.cpp new file mode 100644 index 0000000..6f95ccd --- /dev/null +++ b/tests/bezier-utils-test.cpp @@ -0,0 +1,333 @@ +#include "utest.h" +#include <glib.h> + +/* MenTaLguY disclaims all responsibility for this evil idea for testing + static functions. The main disadvantages are that we retain the + #define's and `using' directives of the included file. */ +#include "../bezier-utils.cpp" + +using Geom::Point; + +static bool range_approx_equal(double const a[], double const b[], unsigned len); + +/* (Returns false if NaN encountered.) */ +template<class T> +static bool range_equal(T const a[], T const b[], unsigned len) { + for (unsigned i = 0; i < len; ++i) { + if ( a[i] != b[i] ) { + return false; + } + } + return true; +} + +inline bool point_approx_equal(Geom::Point const &a, Geom::Point const &b, double const eps) +{ + using Geom::X; using Geom::Y; + return ( Geom_DF_TEST_CLOSE(a[X], b[X], eps) && + Geom_DF_TEST_CLOSE(a[Y], b[Y], eps) ); +} + +static inline double square(double const x) { + return x * x; +} + +/** Determine whether the found control points are the same as previously found on some developer's + machine. Doesn't call utest__fail, just writes a message to stdout for diagnostic purposes: + the most important test is that the root-mean-square of errors in the estimation are low rather + than that the control points found are the same. +**/ +static void compare_ctlpts(Point const est_b[], Point const exp_est_b[]) +{ + unsigned diff_mask = 0; + for (unsigned i = 0; i < 4; ++i) { + for (unsigned d = 0; d < 2; ++d) { + if ( fabs( est_b[i][d] - exp_est_b[i][d] ) > 1.1e-5 ) { + diff_mask |= 1 << ( i * 2 + d ); + } + } + } + if ( diff_mask != 0 ) { + printf("Warning: got different control points from previously-coded (diffs=0x%x).\n", + diff_mask); + printf(" Previous:"); + for (unsigned i = 0; i < 4; ++i) { + printf(" (%g, %g)", exp_est_b[i][0], exp_est_b[i][1]); // localizing ok + } + putchar('\n'); + printf(" Found: "); + for (unsigned i = 0; i < 4; ++i) { + printf(" (%g, %g)", est_b[i][0], est_b[i][1]); // localizing ok + } + putchar('\n'); + } +} + +static void compare_rms(Point const est_b[], double const t[], Point const d[], unsigned const n, + double const exp_rms_error) +{ + double sum_errsq = 0.0; + for (unsigned i = 0; i < n; ++i) { + Point const fit_pt = bezier_pt(3, est_b, t[i]); + Point const diff = fit_pt - d[i]; + sum_errsq += dot(diff, diff); + } + double const rms_error = sqrt( sum_errsq / n ); + UTEST_ASSERT( rms_error <= exp_rms_error + 1.1e-6 ); + if ( rms_error < exp_rms_error - 1.1e-6 ) { + /* The fitter code appears to have improved [or the floating point calculations differ + on this machine from the machine where exp_rms_error was calculated]. */ + printf("N.B. rms_error regression requirement can be decreased: have rms_error=%g.\n", rms_error); // localizing ok + } +} + +int main(int argc, char *argv[]) { + utest_start("bezier-utils.cpp"); + + UTEST_TEST("copy_without_nans_or_adjacent_duplicates") { + Geom::Point const src[] = { + Point(2., 3.), + Point(2., 3.), + Point(0., 0.), + Point(2., 3.), + Point(2., 3.), + Point(1., 9.), + Point(1., 9.) + }; + Point const exp_dest[] = { + Point(2., 3.), + Point(0., 0.), + Point(2., 3.), + Point(1., 9.) + }; + g_assert( G_N_ELEMENTS(src) == 7 ); + Point dest[7]; + struct tst { + unsigned src_ix0; + unsigned src_len; + unsigned exp_dest_ix0; + unsigned exp_dest_len; + } const test_data[] = { + /* src start ix, src len, exp_dest start ix, exp dest len */ + {0, 0, 0, 0}, + {2, 1, 1, 1}, + {0, 1, 0, 1}, + {0, 2, 0, 1}, + {0, 3, 0, 2}, + {1, 3, 0, 3}, + {0, 5, 0, 3}, + {0, 6, 0, 4}, + {0, 7, 0, 4} + }; + for (unsigned i = 0 ; i < G_N_ELEMENTS(test_data) ; ++i) { + tst const &t = test_data[i]; + UTEST_ASSERT( t.exp_dest_len + == copy_without_nans_or_adjacent_duplicates(src + t.src_ix0, + t.src_len, + dest) ); + UTEST_ASSERT(range_equal(dest, + exp_dest + t.exp_dest_ix0, + t.exp_dest_len)); + } + } + + UTEST_TEST("bezier_pt(1)") { + Point const a[] = {Point(2.0, 4.0), + Point(1.0, 8.0)}; + UTEST_ASSERT( bezier_pt(1, a, 0.0) == a[0] ); + UTEST_ASSERT( bezier_pt(1, a, 1.0) == a[1] ); + UTEST_ASSERT( bezier_pt(1, a, 0.5) == Point(1.5, 6.0) ); + double const t[] = {0.5, 0.25, 0.3, 0.6}; + for (unsigned i = 0; i < G_N_ELEMENTS(t); ++i) { + double const ti = t[i], si = 1.0 - ti; + UTEST_ASSERT( bezier_pt(1, a, ti) == si * a[0] + ti * a[1] ); + } + } + + UTEST_TEST("bezier_pt(2)") { + Point const b[] = {Point(1.0, 2.0), + Point(8.0, 4.0), + Point(3.0, 1.0)}; + UTEST_ASSERT( bezier_pt(2, b, 0.0) == b[0] ); + UTEST_ASSERT( bezier_pt(2, b, 1.0) == b[2] ); + UTEST_ASSERT( bezier_pt(2, b, 0.5) == Point(5.0, 2.75) ); + double const t[] = {0.5, 0.25, 0.3, 0.6}; + for (unsigned i = 0; i < G_N_ELEMENTS(t); ++i) { + double const ti = t[i], si = 1.0 - ti; + Point const exp_pt( si*si * b[0] + 2*si*ti * b[1] + ti*ti * b[2] ); + Point const pt(bezier_pt(2, b, ti)); + UTEST_ASSERT(point_approx_equal(pt, exp_pt, 1e-11)); + } + } + + Point const c[] = {Point(1.0, 2.0), + Point(8.0, 4.0), + Point(3.0, 1.0), + Point(-2.0, -4.0)}; + UTEST_TEST("bezier_pt(3)") { + UTEST_ASSERT( bezier_pt(3, c, 0.0) == c[0] ); + UTEST_ASSERT( bezier_pt(3, c, 1.0) == c[3] ); + UTEST_ASSERT( bezier_pt(3, c, 0.5) == Point(4.0, 13.0/8.0) ); + double const t[] = {0.5, 0.25, 0.3, 0.6}; + for (unsigned i = 0; i < G_N_ELEMENTS(t); ++i) { + double const ti = t[i], si = 1.0 - ti; + UTEST_ASSERT( LInfty( bezier_pt(3, c, ti) + - ( si*si*si * c[0] + + 3*si*si*ti * c[1] + + 3*si*ti*ti * c[2] + + ti*ti*ti * c[3] ) ) + < 1e-4 ); + } + } + + struct Err_tst { + Point pt; + double u; + double err; + } const err_tst[] = { + {c[0], 0.0, 0.0}, + {Point(4.0, 13.0/8.0), 0.5, 0.0}, + {Point(4.0, 2.0), 0.5, 9.0/64.0}, + {Point(3.0, 2.0), 0.5, 1.0 + 9.0/64.0}, + {Point(6.0, 2.0), 0.5, 4.0 + 9.0/64.0}, + {c[3], 1.0, 0.0}, + }; + + UTEST_TEST("compute_max_error_ratio") { + Point d[G_N_ELEMENTS(err_tst)]; + double u[G_N_ELEMENTS(err_tst)]; + for (unsigned i = 0; i < G_N_ELEMENTS(err_tst); ++i) { + Err_tst const &t = err_tst[i]; + d[i] = t.pt; + u[i] = t.u; + } + g_assert( G_N_ELEMENTS(u) == G_N_ELEMENTS(d) ); + unsigned max_ix = ~0u; + double const err_ratio = compute_max_error_ratio(d, u, G_N_ELEMENTS(d), c, 1.0, &max_ix); + UTEST_ASSERT( fabs( sqrt(err_tst[4].err) - err_ratio ) < 1e-12 ); + UTEST_ASSERT( max_ix == 4 ); + } + + UTEST_TEST("chord_length_parameterize") { + /* n == 2 */ + { + Point const d[] = {Point(2.9415, -5.8149), + Point(23.021, 4.9814)}; + double u[G_N_ELEMENTS(d)]; + double const exp_u[] = {0.0, 1.0}; + g_assert( G_N_ELEMENTS(u) == G_N_ELEMENTS(exp_u) ); + chord_length_parameterize(d, u, G_N_ELEMENTS(d)); + UTEST_ASSERT(range_equal(u, exp_u, G_N_ELEMENTS(exp_u))); + } + + /* Straight line. */ + { + double const exp_u[] = {0.0, 0.1829, 0.2105, 0.2105, 0.619, 0.815, 0.999, 1.0}; + unsigned const n = G_N_ELEMENTS(exp_u); + Point d[n]; + double u[n]; + Point const a(-23.985, 4.915), b(4.9127, 5.203); + for (unsigned i = 0; i < n; ++i) { + double bi = exp_u[i], ai = 1.0 - bi; + d[i] = ai * a + bi * b; + } + chord_length_parameterize(d, u, n); + UTEST_ASSERT(range_approx_equal(u, exp_u, n)); + } + } + + /* Feed it some points that can be fit exactly with a single bezier segment, and see how + well it manages. */ + Point const src_b[4] = {Point(5., -3.), + Point(8., 0.), + Point(4., 2.), + Point(3., 3.)}; + double const t[] = {0.0, .001, .03, .05, .09, .13, .18, .25, .29, .33, .39, .44, + .51, .57, .62, .69, .75, .81, .91, .93, .97, .98, .999, 1.0}; + unsigned const n = G_N_ELEMENTS(t); + Point d[n]; + for (unsigned i = 0; i < n; ++i) { + d[i] = bezier_pt(3, src_b, t[i]); + } + Point const tHat1(unit_vector( src_b[1] - src_b[0] )); + Point const tHat2(unit_vector( src_b[2] - src_b[3] )); + + UTEST_TEST("generate_bezier") { + Point est_b[4]; + generate_bezier(est_b, d, t, n, tHat1, tHat2, 1.0); + + compare_ctlpts(est_b, src_b); + + /* We're being unfair here in using our t[] rather than best t[] for est_b: we + may over-estimate RMS of errors. */ + compare_rms(est_b, t, d, n, 1e-8); + } + + UTEST_TEST("sp_bezier_fit_cubic_full") { + Point est_b[4]; + int splitpoints[2]; + gint const succ = sp_bezier_fit_cubic_full(est_b, splitpoints, d, n, tHat1, tHat2, square(1.2), 1); + UTEST_ASSERT( succ == 1 ); + + Point const exp_est_b[4] = { + Point(5.000000, -3.000000), + Point(7.5753, -0.4247), + Point(4.77533, 1.22467), + Point(3, 3) + }; + compare_ctlpts(est_b, exp_est_b); + + /* We're being unfair here in using our t[] rather than best t[] for est_b: we + may over-estimate RMS of errors. */ + compare_rms(est_b, t, d, n, .307911); + } + + UTEST_TEST("sp_bezier_fit_cubic") { + Point est_b[4]; + gint const succ = sp_bezier_fit_cubic(est_b, d, n, square(1.2)); + UTEST_ASSERT( succ == 1 ); + + Point const exp_est_b[4] = { + Point(5.000000, -3.000000), + Point(7.57134, -0.423509), + Point(4.77929, 1.22426), + Point(3, 3) + }; + compare_ctlpts(est_b, exp_est_b); + +#if 1 /* A change has been made to right_tangent. I believe that usually this change + will result in better fitting, but it won't do as well for this example where + we happen to be feeding a t=0.999 point to the fitter. */ + printf("TODO: Update this test case for revised right_tangent implementation.\n"); + /* In particular, have a test case to show whether the new implementation + really is likely to be better on average. */ +#else + /* We're being unfair here in using our t[] rather than best t[] for est_b: we + may over-estimate RMS of errors. */ + compare_rms(est_b, t, d, n, .307983); +#endif + } + + return !utest_end(); +} + +/* (Returns false if NaN encountered.) */ +static bool range_approx_equal(double const a[], double const b[], unsigned const len) { + for (unsigned i = 0; i < len; ++i) { + if (!( fabs( a[i] - b[i] ) < 1e-4 )) { + return false; + } + } + return true; +} + +/* + 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 : diff --git a/tests/choose-test.cpp b/tests/choose-test.cpp new file mode 100644 index 0000000..c2b2b50 --- /dev/null +++ b/tests/choose-test.cpp @@ -0,0 +1,79 @@ +/** @file + * @brief Unit tests for the binomial coefficient function. + * Uses the Google Testing Framework + *//* + * 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 "testing.h" +#include <2geom/choose.h> +#include <glib.h> + +using namespace Geom; + +TEST(ChooseTest, PascalsTriangle) { + // check whether the values match Pascal's triangle + for (unsigned i = 0; i < 500; ++i) { + int n = g_random_int_range(3, 100); + int k = g_random_int_range(1, n-1); + + double a = choose<double>(n, k); + double b = choose<double>(n-1, k); + double c = choose<double>(n-1, k-1); + + EXPECT_NEAR((b + c) / a, 1.0, 1e-14); + } +} + +TEST(ChooseTest, Values) { + // test some well-known values + EXPECT_EQ(choose<double>(0, 0), 1); + EXPECT_EQ(choose<double>(1, 0), 1); + EXPECT_EQ(choose<double>(1, 1), 1); + EXPECT_EQ(choose<double>(127, 127), 1); + EXPECT_EQ(choose<double>(92, 0), 1); + EXPECT_EQ(choose<double>(2, 1), 2); + + // number of possible flops in Texas Hold 'Em Poker + EXPECT_EQ(choose<double>(50, 3), 19600.); + EXPECT_EQ(choose<double>(50, 47), 19600.); + // number of possible hands in bridge + EXPECT_EQ(choose<double>(52, 13), 635013559600.); + EXPECT_EQ(choose<double>(52, 39), 635013559600.); + // number of possible Lotto results + EXPECT_EQ(choose<double>(49, 6), 13983816.); + EXPECT_EQ(choose<double>(49, 43), 13983816.); +} + +TEST(ChooseTest, Unsigned) { + auto const BIG = std::numeric_limits<unsigned>::max() - 1; + EXPECT_EQ(choose<unsigned>(BIG, BIG - 1), BIG); + EXPECT_EQ(choose<unsigned>(BIG, BIG), 1); + EXPECT_EQ(choose<unsigned>(BIG, BIG + 1), 0); +} diff --git a/tests/circle-test.cpp b/tests/circle-test.cpp new file mode 100644 index 0000000..5ff6493 --- /dev/null +++ b/tests/circle-test.cpp @@ -0,0 +1,141 @@ +/** @file + * @brief Unit tests for Circle and related functions. + * Uses the Google Testing Framework + *//* + * 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 "testing.h" +#include <2geom/circle.h> +#include <2geom/line.h> + +using namespace Geom; + +TEST(CircleTest, Equality) { + Circle a(4, 5, 6); + Circle b(Point(4, 5), 6); + Circle c(4.00000001, 5, 6); + + EXPECT_EQ(a, b); + EXPECT_NE(a, c); + EXPECT_NE(b, c); +} + +TEST(CircleTest, Nearness) { + Circle a(4, 5, 6); + Circle b(4.000007, 5, 6); + Circle c(4, 5, 6.000007); + Circle d(4.000007, 5, 6.000007); + Circle e(4, 5, 7); + + EXPECT_TRUE(are_near(a, b, 1e-5)); + EXPECT_TRUE(are_near(a, c, 1e-5)); + EXPECT_TRUE(are_near(c, d, 1e-5)); + EXPECT_FALSE(are_near(a, d, 1e-5)); + EXPECT_FALSE(are_near(a, e, 1e-2)); + EXPECT_FALSE(are_near(b, e, 1e-2)); + EXPECT_FALSE(are_near(c, e, 1e-2)); +} + +TEST(CircleTest, UnitCircleTransform) { + Circle c(17, 23, 22); + + Point q = c.pointAt(M_PI/2); + Point p = Point(0, 1) * c.unitCircleTransform(); + Point r = q * c.inverseUnitCircleTransform(); + + EXPECT_FLOAT_EQ(p[X], q[X]); + EXPECT_FLOAT_EQ(p[Y], q[Y]); + EXPECT_FLOAT_EQ(r[X], 0); + EXPECT_FLOAT_EQ(r[Y], 1); +} + +TEST(CircleTest, Coefficients) { + Circle circ(5, 12, 87), circ2; + + Coord a, b, c, d; + circ.coefficients(a, b, c, d); + circ2.setCoefficients(a, b, c, d); + + EXPECT_TRUE(are_near(circ, circ2, 1e-15)); + + for (unsigned i = 0; i < 100; ++i) { + Coord t = -5 + 0.111 * i; + Point p = circ.pointAt(t); + Coord eqres = a * p[X]*p[X] + a*p[Y]*p[Y] + b*p[X] + c*p[Y] + d; + EXPECT_NEAR(eqres, 0, 1e-11); + } +} + +TEST(CircleTest, CircleIntersection) { + Circle a(5, 5, 5), b(15, 5, 5), c(10, 10, 6), d(-5, 5, 2); + std::vector<ShapeIntersection> r1, r2, r3; + + r1 = a.intersect(b); + ASSERT_EQ(r1.size(), 1u); + EXPECT_EQ(r1[0].point(), Point(10,5)); + EXPECT_intersections_valid(a, b, r1, 1e-15); + + r2 = a.intersect(c); + EXPECT_EQ(r2.size(), 2u); + EXPECT_intersections_valid(a, c, r2, 1e-15); + + r3 = b.intersect(c); + EXPECT_EQ(r3.size(), 2u); + EXPECT_intersections_valid(b, c, r3, 4e-15); + + EXPECT_TRUE(a.intersect(d).empty()); + EXPECT_TRUE(b.intersect(d).empty()); + EXPECT_TRUE(c.intersect(d).empty()); +} + +TEST(CircleTest, LineIntersection) { + Circle c(5, 5, 10); + Line l1(Point(-5, -20), Point(-5, 20)); + Line l2(Point(0, 0), Point(10, 2.3)); + Line l3(Point(20, -20), Point(0, -20)); + + EXPECT_TRUE(c.intersects(l1)); + EXPECT_TRUE(c.intersects(l2)); + EXPECT_FALSE(c.intersects(l3)); + + std::vector<ShapeIntersection> r1, r2, r3; + + r1 = c.intersect(l1); + ASSERT_EQ(r1.size(), 1u); + EXPECT_EQ(r1[0].point(), Point(-5, 5)); + EXPECT_intersections_valid(c, l1, r1, 1e-15); + + r2 = c.intersect(l2); + EXPECT_EQ(r2.size(), 2u); + EXPECT_intersections_valid(c, l2, r2, 1e-14); + + r3 = c.intersect(l3); + EXPECT_TRUE(r3.empty()); +} diff --git a/tests/convex-hull-test.cpp b/tests/convex-hull-test.cpp new file mode 100644 index 0000000..2f20f43 --- /dev/null +++ b/tests/convex-hull-test.cpp @@ -0,0 +1,335 @@ +/** @file + * @brief Unit tests for ConvexHull and related functions. + * Uses the Google Testing Framework + *//* + * Authors: + * Nathan Hurst <njh@njhurst.com> + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright 2011-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 "testing.h" +#include <iostream> + +#include <2geom/convex-hull.h> +#include <vector> +#include <iterator> + +#ifndef M_PI +# define M_PI 3.14159265358979323846 +#endif + +using namespace std; +using namespace Geom; + +void points_from_shape(std::vector<Point> &pts, std::string const &shape) { + pts.clear(); + int x = 0, y = 0; + for (char c : shape) { + if (c == '\n') { + x = 0; ++y; + continue; + } + if (c == ' ') { + ++x; + continue; + } + pts.emplace_back(x, y); + ++x; + } +} + +class ConvexHullTest : public ::testing::Test { +protected: + ConvexHullTest() + : null(hulls[0]) + , point(hulls[1]) + , line(hulls[2]) + , triangle(hulls[3]) + , square(hulls[4]) + , hexagon(hulls[5]) + , antihexagon(hulls[6]) + , gem(hulls[7]) + , diamond(hulls[8]) + { + null = ConvexHull(); + + std::vector<Point> pts; + + pts.emplace_back(0,0); + point = ConvexHull(pts); + pts.emplace_back(1,0); + line = ConvexHull(pts); + pts.emplace_back(0,1); + triangle = ConvexHull(pts); + pts.emplace_back(1,1); + square = ConvexHull(pts); + pts.clear(); + + for(int i = 0; i < 6; i++) { + pts.emplace_back(cos(i*M_PI*2/6), sin(i*M_PI*2/6)); + } + hexagon = ConvexHull(pts); + pts.clear(); + + for(int i = 0; i < 6; i++) { + pts.emplace_back(cos((1-i*2)*M_PI/6), sin((1-i*2)*M_PI/6)); + } + antihexagon = ConvexHull(pts); + pts.clear(); + + gem_shape = + " ++++ \n" + "++++++ \n" + "++++++ \n" + "++++++ \n" + " ++++ \n"; + points_from_shape(pts, gem_shape); + gem.swap(pts); + + diamond_shape = + " + \n" + " +++++ \n" + " +++++ \n" + "+++++++ \n" + " +++++ \n" + " +++++ \n" + " + \n"; + points_from_shape(pts, diamond_shape); + diamond.swap(pts); + } + + ConvexHull hulls[9]; + ConvexHull &null, &point, &line, &triangle, &square, &hexagon, &antihexagon, &gem, ⋄ + std::string gem_shape, diamond_shape; +}; + +void check_convex(ConvexHull &/*ch*/) { + // TODO +} + +TEST_F(ConvexHullTest, SizeAndDegeneracy) { + EXPECT_EQ(0u, null.size()); + EXPECT_TRUE(null.empty()); + EXPECT_TRUE(null.isDegenerate()); + EXPECT_FALSE(null.isSingular()); + EXPECT_FALSE(null.isLinear()); + + EXPECT_EQ(1u, point.size()); + EXPECT_FALSE(point.empty()); + EXPECT_TRUE(point.isDegenerate()); + EXPECT_TRUE(point.isSingular()); + EXPECT_FALSE(point.isLinear()); + + EXPECT_EQ(2u, line.size()); + EXPECT_FALSE(line.empty()); + EXPECT_TRUE(line.isDegenerate()); + EXPECT_FALSE(line.isSingular()); + EXPECT_TRUE(line.isLinear()); + + EXPECT_EQ(3u, triangle.size()); + EXPECT_FALSE(triangle.empty()); + EXPECT_FALSE(triangle.isDegenerate()); + EXPECT_FALSE(triangle.isSingular()); + EXPECT_FALSE(triangle.isLinear()); + + EXPECT_EQ(4u, square.size()); + EXPECT_FALSE(square.empty()); + EXPECT_FALSE(square.isDegenerate()); + EXPECT_FALSE(square.isSingular()); + EXPECT_FALSE(square.isLinear()); + + EXPECT_EQ(6u, hexagon.size()); + EXPECT_FALSE(hexagon.empty()); + EXPECT_FALSE(hexagon.isDegenerate()); + EXPECT_FALSE(hexagon.isSingular()); + EXPECT_FALSE(hexagon.isLinear()); + + EXPECT_EQ(6u, antihexagon.size()); + EXPECT_FALSE(antihexagon.empty()); + EXPECT_FALSE(antihexagon.isDegenerate()); + EXPECT_FALSE(antihexagon.isSingular()); + EXPECT_FALSE(antihexagon.isLinear()); + + EXPECT_EQ(8u, gem.size()); + EXPECT_FALSE(gem.empty()); + EXPECT_FALSE(gem.isDegenerate()); + EXPECT_FALSE(gem.isSingular()); + EXPECT_FALSE(gem.isLinear()); + + EXPECT_EQ(8u, diamond.size()); + EXPECT_FALSE(diamond.empty()); + EXPECT_FALSE(diamond.isDegenerate()); + EXPECT_FALSE(diamond.isSingular()); + EXPECT_FALSE(diamond.isLinear()); +} + + +TEST_F(ConvexHullTest, Area) { + EXPECT_EQ(0, null.area()); + EXPECT_EQ(0, point.area()); + EXPECT_EQ(0, line.area()); + EXPECT_EQ(0.5, triangle.area()); + EXPECT_EQ(1, square.area()); + EXPECT_EQ(18, gem.area()); + EXPECT_EQ(24, diamond.area()); + EXPECT_FLOAT_EQ(6*(0.5*1*sin(M_PI/3)), hexagon.area()); + EXPECT_FLOAT_EQ(6*(0.5*1*sin(M_PI/3)), antihexagon.area()); +} + +TEST_F(ConvexHullTest, Bounds) { + //Rect hexbounds(-1,sin(M_PI/3),1,-sin(M_PI/3)); + + EXPECT_EQ(OptRect(), null.bounds()); + EXPECT_EQ(OptRect(0,0,0,0), point.bounds()); + EXPECT_EQ(OptRect(0,0,1,0), line.bounds()); + EXPECT_EQ(OptRect(0,0,1,1), triangle.bounds()); + EXPECT_EQ(OptRect(0,0,1,1), square.bounds()); + EXPECT_EQ(OptRect(0,0,5,4), gem.bounds()); + EXPECT_EQ(OptRect(0,0,6,6), diamond.bounds()); + //EXPECT_TRUE(hexbounds == hexagon.bounds()); + //EXPECT_TRUE(hexbounds == antihexagon.bounds()); +} + +::testing::AssertionResult HullContainsPoint(ConvexHull const &h, Point const &p) { + if (h.contains(p)) { + return ::testing::AssertionSuccess(); + } else { + return ::testing::AssertionFailure() + << "Convex hull:\n" + << h << "\ndoes not contain " << p; + } +} + +TEST_F(ConvexHullTest, PointContainment) { + Point zero(0,0), half(0.5, 0.5), x(0.25, 0.25); + EXPECT_FALSE(HullContainsPoint(null, zero)); + EXPECT_TRUE(HullContainsPoint(point, zero)); + EXPECT_TRUE(HullContainsPoint(line, zero)); + EXPECT_TRUE(HullContainsPoint(triangle, zero)); + EXPECT_TRUE(HullContainsPoint(square, zero)); + EXPECT_FALSE(HullContainsPoint(line, half)); + EXPECT_TRUE(HullContainsPoint(triangle, x)); + EXPECT_TRUE(HullContainsPoint(triangle, half)); + EXPECT_TRUE(HullContainsPoint(square, half)); + EXPECT_TRUE(HullContainsPoint(hexagon, zero)); + EXPECT_TRUE(HullContainsPoint(antihexagon, zero)); + + std::vector<Point> pts; + + points_from_shape(pts, gem_shape); + for (auto & pt : pts) { + EXPECT_TRUE(HullContainsPoint(gem, pt)); + } + + points_from_shape(pts, diamond_shape); + for (auto & pt : pts) { + EXPECT_TRUE(HullContainsPoint(diamond, pt)); + } + + /*EXPECT_FALSE(null.interiorContains(zero)); + EXPECT_FALSE(point.interiorContains(zero)); + EXPECT_FALSE(line.interiorContains(zero)); + EXPECT_FALSE(triangle.interiorContains(zero)); + EXPECT_FALSE(square.interiorContains(zero)); + EXPECT_FALSE(line.interiorContains(half)); + EXPECT_FALSE(triangle.interiorContains(Point(0,0.5))); + EXPECT_FALSE(triangle.interiorContains(half)); + EXPECT_TRUE(square.interiorContains(half));*/ +} + +TEST_F(ConvexHullTest, ExtremePoints) { + Point zero(0,0); + EXPECT_EQ(0., point.top()); + EXPECT_EQ(0., point.right()); + EXPECT_EQ(0., point.bottom()); + EXPECT_EQ(0., point.left()); + EXPECT_EQ(zero, point.topPoint()); + EXPECT_EQ(zero, point.rightPoint()); + EXPECT_EQ(zero, point.bottomPoint()); + EXPECT_EQ(zero, point.leftPoint()); + + // line from 0,0 to 1,0 + EXPECT_EQ(0., line.top()); + EXPECT_EQ(1., line.right()); + EXPECT_EQ(0., line.bottom()); + EXPECT_EQ(0., line.left()); + EXPECT_EQ(Point(1,0), line.topPoint()); + EXPECT_EQ(Point(1,0), line.rightPoint()); + EXPECT_EQ(Point(0,0), line.bottomPoint()); + EXPECT_EQ(Point(0,0), line.leftPoint()); + + // triangle 0,0 1,0 0,1 + EXPECT_EQ(0., triangle.top()); + EXPECT_EQ(1., triangle.right()); + EXPECT_EQ(1., triangle.bottom()); + EXPECT_EQ(0., triangle.left()); + EXPECT_EQ(Point(1,0), triangle.topPoint()); + EXPECT_EQ(Point(1,0), triangle.rightPoint()); + EXPECT_EQ(Point(0,1), triangle.bottomPoint()); + EXPECT_EQ(Point(0,0), triangle.leftPoint()); + + // square 0,0 to 1,1 + EXPECT_EQ(0., square.top()); + EXPECT_EQ(1., square.right()); + EXPECT_EQ(1., square.bottom()); + EXPECT_EQ(0., square.left()); + EXPECT_EQ(Point(1,0), square.topPoint()); + EXPECT_EQ(Point(1,1), square.rightPoint()); + EXPECT_EQ(Point(0,1), square.bottomPoint()); + EXPECT_EQ(Point(0,0), square.leftPoint()); + + EXPECT_EQ(0., gem.top()); + EXPECT_EQ(5., gem.right()); + EXPECT_EQ(4., gem.bottom()); + EXPECT_EQ(0., gem.left()); + EXPECT_EQ(Point(4,0), gem.topPoint()); + EXPECT_EQ(Point(5,3), gem.rightPoint()); + EXPECT_EQ(Point(1,4), gem.bottomPoint()); + EXPECT_EQ(Point(0,1), gem.leftPoint()); + + EXPECT_EQ(0., diamond.top()); + EXPECT_EQ(6., diamond.right()); + EXPECT_EQ(6., diamond.bottom()); + EXPECT_EQ(0., diamond.left()); + EXPECT_EQ(Point(3,0), diamond.topPoint()); + EXPECT_EQ(Point(6,3), diamond.rightPoint()); + EXPECT_EQ(Point(3,6), diamond.bottomPoint()); + EXPECT_EQ(Point(0,3), diamond.leftPoint()); +} + + +/* + 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 : diff --git a/tests/coord-test.cpp b/tests/coord-test.cpp new file mode 100644 index 0000000..c96b095 --- /dev/null +++ b/tests/coord-test.cpp @@ -0,0 +1,90 @@ +/** @file + * @brief Unit tests for functions related to Coord. + * Uses the Google Testing Framework + *//* + * Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright 2014 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/coord.h> +#include <climits> +#include <stdint.h> +#include <glib.h> +#include <iostream> + +namespace Geom { + +TEST(CoordTest, StringRoundtripShortest) { + union { + uint64_t u; + double d; + }; + for (unsigned i = 0; i < 100000; ++i) { + u = uint64_t(g_random_int()) | (uint64_t(g_random_int()) << 32); + if (!std::isfinite(d)) continue; + + std::string str = format_coord_shortest(d); + double x = parse_coord(str); + if (x != d) { + std::cout << std::endl << d << " -> " << str << " -> " << x << std::endl; + } + EXPECT_EQ(d, x); + } +} + +TEST(CoordTest, StringRoundtripNice) { + union { + uint64_t u; + double d; + }; + for (unsigned i = 0; i < 100000; ++i) { + u = uint64_t(g_random_int()) | (uint64_t(g_random_int()) << 32); + if (!std::isfinite(d)) continue; + + std::string str = format_coord_nice(d); + double x = parse_coord(str); + if (x != d) { + std::cout << std::endl << d << " -> " << str << " -> " << x << std::endl; + } + EXPECT_EQ(d, x); + } +} + +} // end 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 : diff --git a/tests/dependent-project/.gitignore b/tests/dependent-project/.gitignore new file mode 100644 index 0000000..0bb214c --- /dev/null +++ b/tests/dependent-project/.gitignore @@ -0,0 +1,2 @@ +build-as-subproject +build-with-find-package
\ No newline at end of file diff --git a/tests/dependent-project/CMakeLists.txt b/tests/dependent-project/CMakeLists.txt new file mode 100644 index 0000000..c371114 --- /dev/null +++ b/tests/dependent-project/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.1) +project(test_dep_2geom CXX C) +set(CMAKE_CXX_STANDARD 17) + +option(2GEOM_AS_SUBPROJECT "include 2geom as subproject" OFF) + +if (2GEOM_AS_SUBPROJECT) + message("Using 2geom as subdirectory") + set(2GEOM_BUILD_SHARED ON CACHE BOOL "Build 2geom shared version") + add_subdirectory("../../" 2geom) +else() + message("Using installed 2geom") + find_package(2Geom REQUIRED) +endif() + +add_library(my_lib SHARED my_lib.cpp) +add_executable(main main.cpp) +target_link_libraries(main my_lib) +target_link_libraries(my_lib PUBLIC 2Geom::2geom) + +install(TARGETS + main + my_lib + RUNTIME DESTINATION bin + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + ) diff --git a/tests/dependent-project/main.cpp b/tests/dependent-project/main.cpp new file mode 100644 index 0000000..6b6469c --- /dev/null +++ b/tests/dependent-project/main.cpp @@ -0,0 +1,12 @@ +#include <2geom/2geom.h> +#include <iostream> +#include "my_lib.h" + +int main() { + Geom::Rect rect1(0, 0, 1, 1); + Geom::Rect rect2(0.5, 0.5, 1.5, 1.5); + + std::cout << sum_of_three_points(Geom::Point(1, 1), Geom::Point(1, 2), Geom::Point(2, 3)); + + return rect1.intersects(rect2) ? 0 : 1; +} diff --git a/tests/dependent-project/my_lib.cpp b/tests/dependent-project/my_lib.cpp new file mode 100644 index 0000000..d5af62a --- /dev/null +++ b/tests/dependent-project/my_lib.cpp @@ -0,0 +1,6 @@ +#include "my_lib.h" + +using namespace Geom; +Point sum_of_three_points(const Point&a, const Point&b, const Point&c){ + return a+b+c; +}
\ No newline at end of file diff --git a/tests/dependent-project/my_lib.h b/tests/dependent-project/my_lib.h new file mode 100644 index 0000000..2278aaf --- /dev/null +++ b/tests/dependent-project/my_lib.h @@ -0,0 +1,4 @@ +#pragma once +#include <2geom/2geom.h> + +Geom::Point sum_of_three_points(const Geom::Point&a, const Geom::Point&b, const Geom::Point&c);
\ No newline at end of file diff --git a/tests/ellipse-test.cpp b/tests/ellipse-test.cpp new file mode 100644 index 0000000..38eca0e --- /dev/null +++ b/tests/ellipse-test.cpp @@ -0,0 +1,410 @@ +/** @file + * @brief Unit tests for Ellipse and related functions + * Uses the Google Testing Framework + *//* + * 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 <iostream> +#include <glib.h> + +#include <2geom/angle.h> +#include <2geom/ellipse.h> +#include <2geom/elliptical-arc.h> +#include <memory> + +#include "testing.h" + +#ifndef M_SQRT2 +# define M_SQRT2 1.41421356237309504880 +#endif + +using namespace Geom; + +TEST(EllipseTest, Arcs) { + Ellipse e(Point(5,10), Point(5, 10), 0); + + std::unique_ptr<EllipticalArc> arc1(e.arc(Point(5,0), Point(0,0), Point(0,10))); + + EXPECT_EQ(arc1->initialPoint(), Point(5,0)); + EXPECT_EQ(arc1->finalPoint(), Point(0,10)); + EXPECT_EQ(arc1->boundsExact(), Rect::from_xywh(0,0,5,10)); + EXPECT_EQ(arc1->center(), e.center()); + EXPECT_EQ(arc1->largeArc(), false); + EXPECT_EQ(arc1->sweep(), false); + + std::unique_ptr<EllipticalArc> arc1r(e.arc(Point(0,10), Point(0,0), Point(5,0))); + + EXPECT_EQ(arc1r->boundsExact(), arc1->boundsExact()); + EXPECT_EQ(arc1r->sweep(), true); + EXPECT_EQ(arc1r->largeArc(), false); + + std::unique_ptr<EllipticalArc> arc2(e.arc(Point(5,0), Point(10,20), Point(0,10))); + + EXPECT_EQ(arc2->boundsExact(), Rect::from_xywh(0,0,10,20)); + EXPECT_EQ(arc2->largeArc(), true); + EXPECT_EQ(arc2->sweep(), true); + + std::unique_ptr<EllipticalArc> arc2r(e.arc(Point(0,10), Point(10,20), Point(5,0))); + + EXPECT_EQ(arc2r->boundsExact(), arc2->boundsExact()); + EXPECT_EQ(arc2r->largeArc(), true); + EXPECT_EQ(arc2r->sweep(), false); + + // exactly half arc + std::unique_ptr<EllipticalArc> arc3(e.arc(Point(5,0), Point(0,10), Point(5,20))); + + EXPECT_EQ(arc3->boundsExact(), Rect::from_xywh(0,0,5,20)); + EXPECT_EQ(arc3->largeArc(), false); + EXPECT_EQ(arc3->sweep(), false); + + // inner point exactly at midpoint between endpoints + std::unique_ptr<EllipticalArc> arc4(e.arc(Point(5,0), Point(2.5,5), Point(0,10))); + + EXPECT_EQ(arc4->initialPoint(), Point(5,0)); + EXPECT_EQ(arc4->finalPoint(), Point(0,10)); + EXPECT_EQ(arc4->boundsExact(), Rect::from_xywh(0,0,5,10)); + EXPECT_EQ(arc4->largeArc(), false); + EXPECT_EQ(arc4->sweep(), false); + + std::unique_ptr<EllipticalArc> arc4r(e.arc(Point(0,10), Point(2.5,5), Point(5,0))); + + EXPECT_EQ(arc4r->initialPoint(), Point(0,10)); + EXPECT_EQ(arc4r->finalPoint(), Point(5,0)); + EXPECT_EQ(arc4r->boundsExact(), Rect::from_xywh(0,0,5,10)); + EXPECT_EQ(arc4r->largeArc(), false); + EXPECT_EQ(arc4r->sweep(), true); +} + +TEST(EllipseTest, AreNear) { + Ellipse e1(Point(5.000001,10), Point(5,10), Angle::from_degrees(45)); + Ellipse e2(Point(5.000000,10), Point(5,10), Angle::from_degrees(225)); + Ellipse e3(Point(4.999999,10), Point(10,5), Angle::from_degrees(135)); + Ellipse e4(Point(5.000001,10), Point(10,5), Angle::from_degrees(315)); + + EXPECT_TRUE(are_near(e1, e2, 1e-5)); + EXPECT_TRUE(are_near(e1, e3, 1e-5)); + EXPECT_TRUE(are_near(e1, e4, 1e-5)); + + Ellipse c1(Point(20.000001,35.000001), Point(5.000001,4.999999), Angle::from_degrees(180.00001)); + Ellipse c2(Point(19.999999,34.999999), Point(4.999999,5.000001), Angle::from_degrees(179.99999)); + //std::cout << c1 << "\n" << c2 << std::endl; + EXPECT_TRUE(are_near(c1, c2, 2e-5)); + + EXPECT_FALSE(are_near(c1, e1, 1e-5)); + EXPECT_FALSE(are_near(c2, e1, 1e-5)); + EXPECT_FALSE(are_near(c1, e2, 1e-5)); + EXPECT_FALSE(are_near(c2, e2, 1e-5)); + EXPECT_FALSE(are_near(c1, e3, 1e-5)); + EXPECT_FALSE(are_near(c2, e3, 1e-5)); + EXPECT_FALSE(are_near(c1, e4, 1e-5)); + EXPECT_FALSE(are_near(c2, e4, 1e-5)); +} + +TEST(EllipseTest, Transformations) { + Ellipse e(Point(5,10), Point(5,10), Angle::from_degrees(45)); + + Ellipse er = e * Rotate::around(Point(5,10), Angle::from_degrees(45)); + Ellipse ercmp(Point(5,10), Point(5,10), Angle::from_degrees(90)); + //std::cout << e << "\n" << er << "\n" << ercmp << std::endl; + EXPECT_TRUE(are_near(er, ercmp, 1e-12)); + + Ellipse eflip = e * Affine(Scale(-1,1)); + Ellipse eflipcmp(Point(-5, 10), Point(5,10), Angle::from_degrees(135)); + EXPECT_TRUE(are_near(eflip, eflipcmp, 1e-12)); +} + +TEST(EllipseTest, TimeAt) { + Ellipse e(Point(4, 17), Point(22, 34), 2); + + for (unsigned i = 0; i < 100; ++i) { + Coord t = g_random_double_range(0, 2*M_PI); + Point p = e.pointAt(t); + Coord t2 = e.timeAt(p); + EXPECT_FLOAT_EQ(t, t2); + } +} + +TEST(EllipseTest, LineIntersection) { + Ellipse e(Point(0, 0), Point(3, 2), 0); + Line l(Point(0, -2), Point(1, 0)); + + std::vector<ShapeIntersection> xs = e.intersect(l); + + ASSERT_EQ(xs.size(), 2ul); + + // due to numeric imprecision when evaluating Ellipse, + // the points may deviate by around 2e-16 + EXPECT_NEAR(xs[0].point()[X], 0, 1e-15); + EXPECT_NEAR(xs[0].point()[Y], -2, 1e-15); + EXPECT_NEAR(xs[1].point()[X], 9./5, 1e-15); + EXPECT_NEAR(xs[1].point()[Y], 8./5, 1e-15); + + EXPECT_intersections_valid(e, l, xs, 1e-15); + + // Test with a degenerate ellipse + auto degen = Ellipse({0, 0}, {3, 2}, 0); + degen *= Scale(1.0, 0.0); // Squash to the X-axis interval [-3, 3]. + + g_random_set_seed(0xCAFECAFE); + // Intersect with a line + for (size_t _ = 0; _ < 10'000; _++) { + auto line = Line(Point(g_random_double_range(-3.0, 3.0), g_random_double_range(-3.0, -1.0)), + Point(g_random_double_range(-3.0, 3.0), g_random_double_range(1.0, 3.0))); + auto xings = degen.intersect(line); + EXPECT_EQ(xings.size(), 2u); + EXPECT_intersections_valid(degen, line, xings, 1e-14); + } + // Intersect with another, non-degenerate ellipse + for (size_t _ = 0; _ < 10'000; _++) { + auto other = Ellipse(Point(g_random_double_range(-1.0, 1.0), g_random_double_range(-1.0, 1.0)), + Point(g_random_double_range(1.0, 2.0), g_random_double_range(1.0, 3.0)), 0); + auto xings = degen.intersect(other); + EXPECT_intersections_valid(degen, other, xings, 1e-14); + } + // Intersect with another ellipse which is also degenerate + for (size_t _ = 0; _ < 10'000; _++) { + auto other = Ellipse({0, 0}, {1, 1}, 0); // Unit circle + other *= Scale(0.0, g_random_double_range(0.5, 4.0)); // Squash to Y axis + other *= Rotate(g_random_double_range(-1.5, 1.5)); // Rotate a little (still passes through the origin) + other *= Translate(g_random_double_range(-2.9, 2.9), 0.0); + auto xings = degen.intersect(other); + EXPECT_EQ(xings.size(), 4u); + EXPECT_intersections_valid(degen, other, xings, 1e-14); + } +} + +TEST(EllipseTest, EllipseIntersection) { + Ellipse e1; + Ellipse e2; + std::vector<ShapeIntersection> xs; + + e1.set(Point(300, 300), Point(212, 70), -0.785); + e2.set(Point(250, 300), Point(230, 90), 1.321); + xs = e1.intersect(e2); + EXPECT_EQ(xs.size(), 4ul); + EXPECT_intersections_valid(e1, e2, xs, 4e-10); + + e1.set(Point(0, 0), Point(1, 1), 0); + e2.set(Point(0, 1), Point(1, 1), 0); + xs = e1.intersect(e2); + EXPECT_EQ(xs.size(), 2ul); + EXPECT_intersections_valid(e1, e2, xs, 1e-10); + + e1.set(Point(0, 0), Point(1, 1), 0); + e2.set(Point(1, 0), Point(1, 1), 0); + xs = e1.intersect(e2); + EXPECT_EQ(xs.size(), 2ul); + EXPECT_intersections_valid(e1, e2, xs, 1e-10); + + // === Test detection of external tangency between ellipses === + // Perpendicular major axes + e1.set({0, 0}, {5, 3}, 0); // rightmost point (5, 0) + e2.set({6, 0}, {1, 2}, 0); // leftmost point (5, 0) + xs = e1.intersect(e2); + ASSERT_GT(xs.size(), 0); + EXPECT_intersections_valid(e1, e2, xs, 1e-10); + EXPECT_TRUE(are_near(xs[0].point(), Point(5, 0))); + + // Collinear major axes + e1.set({30, 0}, {9, 1}, 0); // leftmost point (21, 0) + e2.set({18, 0}, {3, 2}, 0); // rightmost point (21, 0) + xs = e1.intersect(e2); + ASSERT_GT(xs.size(), 0); + EXPECT_intersections_valid(e1, e2, xs, 1e-10); + EXPECT_TRUE(are_near(xs[0].point(), Point(21, 0))); + + // Circles not aligned to an axis (Pythagorean triple: 3^2 + 4^2 == 5^2) + e1.set({0, 0}, {3, 3}, 0); // radius 3 + e2.set({3, 4}, {2, 2}, 0); // radius 2 + // We know 2 + 3 == 5 == distance((0, 0), (3, 4)) so there's an external tangency + // between these circles, at a point at distance 3 from the origin, on the line x = 0.75 y. + xs = e1.intersect(e2); + ASSERT_GT(xs.size(), 0); + EXPECT_intersections_valid(e1, e2, xs, 1e-6); + + // === Test the detection of internal tangency between ellipses === + // Perpendicular major axes + e1.set({0, 0}, {8, 17}, 0); // rightmost point (8, 0) + e2.set({6, 0}, {2, 1}, 0); // rightmost point (8, 0) + xs = e1.intersect(e2); + ASSERT_GT(xs.size(), 0); + EXPECT_intersections_valid(e1, e2, xs, 1e-10); + EXPECT_TRUE(are_near(xs[0].point(), Point(8, 0))); + + // Collinear major axes + e1.set({30, 0}, {9, 5}, 0); // rightmost point (39, 0) + e2.set({36, 0}, {3, 1}, 0); // rightmost point (39, 0) + xs = e1.intersect(e2); + ASSERT_GT(xs.size(), 0); + EXPECT_intersections_valid(e1, e2, xs, 1e-6); + EXPECT_TRUE(are_near(xs[0].point(), Point(39, 0))); + + // Circles not aligned to an axis (Pythagorean triple: 3^2 + 4^2 == 5^2) + e1.set({4, 3}, {5, 5}, 0); // Passes through (0, 0), center on the line y = 0.75 x + e2.set({8, 6}, {10, 10}, 0); // Also passes through (0, 0), center on the same line. + xs = e1.intersect(e2); + ASSERT_GT(xs.size(), 0); + EXPECT_intersections_valid(e1, e2, xs, 1e-6); + EXPECT_TRUE(are_near(xs[0].point(), Point(0, 0))); +} + +TEST(EllipseTest, BezierIntersection) { + Ellipse e(Point(300, 300), Point(212, 70), -3.926); + D2<Bezier> b(Bezier(100, 300, 100, 500), Bezier(100, 100, 500, 500)); + + std::vector<ShapeIntersection> xs = e.intersect(b); + + EXPECT_EQ(xs.size(), 2ul); + EXPECT_intersections_valid(e, b, xs, 6e-12); +} + +TEST(EllipseTest, Coefficients) { + std::vector<Ellipse> es; + es.emplace_back(Point(-15,25), Point(10,15), Angle::from_degrees(45).radians0()); + es.emplace_back(Point(-10,33), Point(40,20), M_PI); + es.emplace_back(Point(10,-33), Point(40,20), Angle::from_degrees(135).radians0()); + es.emplace_back(Point(-10,-33), Point(50,10), Angle::from_degrees(330).radians0()); + + for (auto & i : es) { + Coord a, b, c, d, e, f; + i.coefficients(a, b, c, d, e, f); + Ellipse te(a, b, c, d, e, f); + EXPECT_near(i, te, 1e-10); + for (Coord t = -5; t < 5; t += 0.125) { + Point p = i.pointAt(t); + Coord eq = a*p[X]*p[X] + b*p[X]*p[Y] + c*p[Y]*p[Y] + + d*p[X] + e*p[Y] + f; + EXPECT_NEAR(eq, 0, 1e-10); + } + } +} + +TEST(EllipseTest, UnitCircleTransform) { + std::vector<Ellipse> es; + es.emplace_back(Point(-15,25), Point(10,15), Angle::from_degrees(45)); + es.emplace_back(Point(-10,33), Point(40,20), M_PI); + es.emplace_back(Point(10,-33), Point(40,20), Angle::from_degrees(135)); + es.emplace_back(Point(-10,-33), Point(50,10), Angle::from_degrees(330)); + + for (auto & e : es) { + EXPECT_near(e.unitCircleTransform() * e.inverseUnitCircleTransform(), Affine::identity(), 1e-8); + + for (Coord t = -1; t < 10; t += 0.25) { + Point p = e.pointAt(t); + p *= e.inverseUnitCircleTransform(); + EXPECT_near(p.length(), 1., 1e-10); + p *= e.unitCircleTransform(); + EXPECT_near(e.pointAt(t), p, 1e-10); + } + } +} + +TEST(EllipseTest, PointAt) { + Ellipse a(Point(0,0), Point(10,20), 0); + EXPECT_near(a.pointAt(0), Point(10,0), 1e-10); + EXPECT_near(a.pointAt(M_PI/2), Point(0,20), 1e-10); + EXPECT_near(a.pointAt(M_PI), Point(-10,0), 1e-10); + EXPECT_near(a.pointAt(3*M_PI/2), Point(0,-20), 1e-10); + + Ellipse b(Point(0,0), Point(10,20), M_PI/2); + EXPECT_near(b.pointAt(0), Point(0,10), 1e-10); + EXPECT_near(b.pointAt(M_PI/2), Point(-20,0), 1e-10); + EXPECT_near(b.pointAt(M_PI), Point(0,-10), 1e-10); + EXPECT_near(b.pointAt(3*M_PI/2), Point(20,0), 1e-10); +} + +TEST(EllipseTest, UnitTangentAt) { + Ellipse a(Point(14,-7), Point(20,10), 0); + Ellipse b(Point(-77,23), Point(40,10), Angle::from_degrees(45)); + + EXPECT_near(a.unitTangentAt(0), Point(0,1), 1e-12); + EXPECT_near(a.unitTangentAt(M_PI/2), Point(-1,0), 1e-12); + EXPECT_near(a.unitTangentAt(M_PI), Point(0,-1), 1e-12); + EXPECT_near(a.unitTangentAt(3*M_PI/2), Point(1,0), 1e-12); + + EXPECT_near(b.unitTangentAt(0), Point(-M_SQRT2/2, M_SQRT2/2), 1e-12); + EXPECT_near(b.unitTangentAt(M_PI/2), Point(-M_SQRT2/2, -M_SQRT2/2), 1e-12); + EXPECT_near(b.unitTangentAt(M_PI), Point(M_SQRT2/2, -M_SQRT2/2), 1e-12); + EXPECT_near(b.unitTangentAt(3*M_PI/2), Point(M_SQRT2/2, M_SQRT2/2), 1e-12); +} + +TEST(EllipseTest, Bounds) +{ + // Create example ellipses + std::vector<Ellipse> es; + es.emplace_back(Point(-15,25), Point(10,15), Angle::from_degrees(45)); + es.emplace_back(Point(-10,33), Point(40,20), M_PI); + es.emplace_back(Point(10,-33), Point(40,20), Angle::from_degrees(111)); + es.emplace_back(Point(-10,-33), Point(50,10), Angle::from_degrees(222)); + + // for reproducibility + g_random_set_seed(1234); + + for (auto & e : es) { + Rect r = e.boundsExact(); + Rect f = e.boundsFast(); + for (unsigned j = 0; j < 10000; ++j) { + Coord t = g_random_double_range(-M_PI, M_PI); + auto const p = e.pointAt(t); + EXPECT_TRUE(r.contains(p)); + EXPECT_TRUE(f.contains(p)); + } + } + + Ellipse e(Point(0,0), Point(10, 10), M_PI); + Rect bounds = e.boundsExact(); + Rect coarse = e.boundsFast(); + EXPECT_EQ(bounds, Rect(Point(-10,-10), Point(10,10))); + EXPECT_TRUE(bounds.contains(e.pointAt(0))); + EXPECT_TRUE(bounds.contains(e.pointAt(M_PI/2))); + EXPECT_TRUE(bounds.contains(e.pointAt(M_PI))); + EXPECT_TRUE(bounds.contains(e.pointAt(3*M_PI/2))); + EXPECT_TRUE(bounds.contains(e.pointAt(2*M_PI))); + EXPECT_TRUE(coarse.contains(e.pointAt(0))); + EXPECT_TRUE(coarse.contains(e.pointAt(M_PI/2))); + EXPECT_TRUE(coarse.contains(e.pointAt(M_PI))); + EXPECT_TRUE(coarse.contains(e.pointAt(3*M_PI/2))); + EXPECT_TRUE(coarse.contains(e.pointAt(2*M_PI))); + + e = Ellipse(Point(0,0), Point(10, 10), M_PI/2); + bounds = e.boundsExact(); + coarse = e.boundsFast(); + EXPECT_EQ(bounds, Rect(Point(-10,-10), Point(10,10))); + EXPECT_TRUE(bounds.contains(e.pointAt(0))); + EXPECT_TRUE(bounds.contains(e.pointAt(M_PI/2))); + EXPECT_TRUE(bounds.contains(e.pointAt(M_PI))); + EXPECT_TRUE(bounds.contains(e.pointAt(3*M_PI/2))); + EXPECT_TRUE(bounds.contains(e.pointAt(2*M_PI))); + EXPECT_TRUE(coarse.contains(e.pointAt(0))); + EXPECT_TRUE(coarse.contains(e.pointAt(M_PI/2))); + EXPECT_TRUE(coarse.contains(e.pointAt(M_PI))); + EXPECT_TRUE(coarse.contains(e.pointAt(3*M_PI/2))); + EXPECT_TRUE(coarse.contains(e.pointAt(2*M_PI))); +} diff --git a/tests/elliptical-arc-test.cpp b/tests/elliptical-arc-test.cpp new file mode 100644 index 0000000..1f6eff7 --- /dev/null +++ b/tests/elliptical-arc-test.cpp @@ -0,0 +1,275 @@ +/** @file + * @brief Unit tests for EllipticalArc. + * Uses the Google Testing Framework + *//* + * 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 "testing.h" +#include <2geom/elliptical-arc.h> +#include <glib.h> + +using namespace Geom; + +TEST(EllipticalArcTest, PointAt) { + EllipticalArc a(Point(0,0), Point(10,20), M_PI/2, false, true, Point(-40,0)); + EXPECT_near(a.pointAt(0), a.initialPoint(), 1e-14); + EXPECT_near(a.pointAt(1), a.finalPoint(), 1e-14); + EXPECT_near(a.pointAt(0.5), Point(-20,10), 1e-14); + + EllipticalArc b(Point(0,0), Point(10,20), 0, false, true, Point(-40,0)); + EXPECT_near(b.pointAt(0), b.initialPoint(), 1e-14); + EXPECT_near(b.pointAt(1), b.finalPoint(), 1e-14); + EXPECT_near(b.pointAt(0.5), Point(-20,40), 1e-14); + + EllipticalArc c(Point(200,0), Point(40,20), Angle::from_degrees(90), false, false, Point(200,100)); + EXPECT_near(c.pointAt(0), c.initialPoint(), 1e-13); + EXPECT_near(c.pointAt(1), c.finalPoint(), 1e-13); + EXPECT_near(c.pointAt(0.5), Point(175, 50), 1e-13); +} + +TEST(EllipticalArc, Transform) { + EllipticalArc a(Point(0,0), Point(10,20), M_PI/2, false, true, Point(-40,0)); + EllipticalArc b(Point(-40,0), Point(10,20), M_PI/2, false, true, Point(0,0)); + EllipticalArc c = a; + Affine m = Rotate::around(Point(-20,0), M_PI); + c.transform(m); + + for (unsigned i = 0; i <= 100; ++i) { + Coord t = i/100.; + EXPECT_near(c.pointAt(t), b.pointAt(t), 1e-12); + EXPECT_near(a.pointAt(t)*m, c.pointAt(t), 1e-12); + } +} + +TEST(EllipticalArcTest, Duplicate) { + EllipticalArc a(Point(0,0), Point(10,20), M_PI/2, true, false, Point(-40,0)); + EllipticalArc *b = static_cast<EllipticalArc*>(a.duplicate()); + EXPECT_EQ(a, *b); + delete b; +} + +TEST(EllipticalArcTest, LineSegmentIntersection) { + std::vector<CurveIntersection> r1; + EllipticalArc a3(Point(0,0), Point(5,1.5), 0, true, true, Point(0,2)); + LineSegment ls(Point(0,5), Point(7,-3)); + r1 = a3.intersect(ls); + EXPECT_EQ(r1.size(), 2u); + EXPECT_intersections_valid(a3, ls, r1, 1e-10); + + g_random_set_seed(0xB747A380); + // Test with randomized arcs and segments. + for (size_t _ = 0; _ < 10'000; _++) { + auto arc = EllipticalArc({g_random_double_range(1.0, 5.0), 0.0}, + {g_random_double_range(6.0, 8.0), g_random_double_range(2.0, 7.0)}, + g_random_double_range(-0.5, 0.5), true, g_random_boolean(), + {g_random_double_range(-5.0, -1.0), 0.0}); + Coord x = g_random_double_range(15, 30); + Coord y = g_random_double_range(10, 20); + auto seg = LineSegment(Point(-x, y), Point(x, -y)); + auto xings = arc.intersect(seg); + EXPECT_EQ(xings.size(), 1u); + EXPECT_intersections_valid(arc, seg, xings, 1e-12); + } + + // Test with degenerate arcs + EllipticalArc x_squash_pos{{3.0, 0.0}, {3.0, 2.0}, 0, true, true, {-3.0, 0.0}}; + EllipticalArc x_squash_neg{{3.0, 0.0}, {3.0, 2.0}, 0, true, false, {-3.0, 0.0}}; + auto const squash_to_x = Scale(1.0, 0.0); + x_squash_pos *= squash_to_x; // squash to X axis interval [-3, 3]. + x_squash_neg *= squash_to_x; + + for (size_t _ = 0; _ < 10'000; _++) { + auto seg = LineSegment(Point(g_random_double_range(-3.0, 3.0), g_random_double_range(-3.0, -1.0)), + Point(g_random_double_range(-3.0, 3.0), g_random_double_range(1.0, 3.0))); + auto xings = x_squash_pos.intersect(seg); + EXPECT_EQ(xings.size(), 1u); + EXPECT_intersections_valid(x_squash_pos, seg, xings, 1e-12); + + std::unique_ptr<Curve> rev{x_squash_pos.reverse()}; + xings = rev->intersect(seg); + EXPECT_EQ(xings.size(), 1u); + EXPECT_intersections_valid(*rev, seg, xings, 1e-12); + + xings = x_squash_neg.intersect(seg); + EXPECT_EQ(xings.size(), 1u); + EXPECT_intersections_valid(x_squash_neg, seg, xings, 1e-12); + + rev.reset(x_squash_neg.reverse()); + xings = rev->intersect(seg); + EXPECT_EQ(xings.size(), 1u); + EXPECT_intersections_valid(*rev, seg, xings, 1e-12); + } + + // Now test with an arc squashed to the Y-axis. + EllipticalArc y_squash_pos{{0.0, -2.0}, {3.0, 2.0}, 0, true, true, {0.0, 2.0}}; + EllipticalArc y_squash_neg{{0.0, -2.0}, {3.0, 2.0}, 0, true, false, {0.0, 2.0}}; + auto const squash_to_y = Scale(0.0, 1.0); + y_squash_pos *= squash_to_y; // Y-axis interval [-2, 2]. + y_squash_neg *= squash_to_y; + + for (size_t _ = 0; _ < 10'000; _++) { + auto seg = LineSegment(Point(g_random_double_range(-3.0, -1.0), g_random_double_range(-2.0, 2.0)), + Point(g_random_double_range(1.0, 3.0), g_random_double_range(-2.0, 2.0))); + auto xings = y_squash_pos.intersect(seg, 1e-10); + EXPECT_EQ(xings.size(), 1u); + EXPECT_intersections_valid(y_squash_pos, seg, xings, 1e-12); + + std::unique_ptr<Curve> rev{y_squash_pos.reverse()}; + xings = rev->intersect(seg, 1e-12); + EXPECT_EQ(xings.size(), 1u); + EXPECT_intersections_valid(*rev, seg, xings, 1e-12); + + xings = y_squash_neg.intersect(seg, 1e-12); + EXPECT_EQ(xings.size(), 1u); + EXPECT_intersections_valid(y_squash_neg, seg, xings, 1e-12); + + rev.reset(y_squash_neg.reverse()); + xings = rev->intersect(seg, 1e-12); + EXPECT_EQ(xings.size(), 1u); + EXPECT_intersections_valid(*rev, seg, xings, 1e-12); + } + + // Test whether the coincidence between the common endpoints of an + // arc and a segment is correctly detected as an intersection. + { + Point const from{1, 0}; + Point const to{0.30901699437494745, 0.9510565162951535}; + auto arc = EllipticalArc(from, {1, 1}, 0, false, true, to); + auto seg = LineSegment({0, 0}, to); + auto xings = arc.intersect(seg); + ASSERT_EQ(xings.size(), 1); + EXPECT_TRUE(are_near(xings[0].point(), to, 1e-12)); + EXPECT_TRUE(are_near(xings[0].first, 1.0, 1e-24)); + EXPECT_TRUE(are_near(xings[0].second, 1.0, 1e-24)); + + auto seg2 = LineSegment(Point{1, 1}, from); + xings = arc.intersect(seg2); + ASSERT_EQ(xings.size(), 1); + EXPECT_TRUE(are_near(xings[0].point(), from, 1e-12)); + EXPECT_TRUE(are_near(xings[0].first, 0.0, 1e-24)); + EXPECT_TRUE(are_near(xings[0].second, 1.0, 1e-24)); + } +} + +TEST(EllipticalArcTest, ArcIntersection) { + std::vector<CurveIntersection> r1, r2; + + EllipticalArc a1(Point(0,0), Point(6,3), 0.1, false, false, Point(10,0)); + EllipticalArc a2(Point(0,2), Point(6,3), -0.1, false, true, Point(10,2)); + r1 = a1.intersect(a2); + EXPECT_EQ(r1.size(), 2u); + EXPECT_intersections_valid(a1, a2, r1, 1e-10); + + EllipticalArc a3(Point(0,0), Point(5,1.5), 0, true, true, Point(0,2)); + EllipticalArc a4(Point(3,5), Point(5,1.5), M_PI/2, true, true, Point(5,0)); + r2 = a3.intersect(a4); + EXPECT_EQ(r2.size(), 3u); + EXPECT_intersections_valid(a3, a4, r2, 1e-10); + + // Make sure intersections are found between two identical arcs on the unit circle. + EllipticalArc const upper(Point(1, 0), Point(1, 1), 0, true, true, Point(-1, 0)); + auto self_intersect = upper.intersect(upper); + EXPECT_EQ(self_intersect.size(), 2u); + + // Make sure intersections are found between overlapping arcs. + EllipticalArc const right(Point(0, -1), Point(1, 1), 0, true, true, Point(0, 1)); + auto quartering_overlap_xings = right.intersect(upper); + EXPECT_EQ(quartering_overlap_xings.size(), 2u); + + // Make sure intersecections are found between an arc and its sub-arc. + EllipticalArc const middle(upper.pointAtAngle(0.25 * M_PI), Point(1, 1), 0, true, true, upper.pointAtAngle(-0.25 * M_PI)); + EXPECT_EQ(middle.intersect(upper).size(), 2u); + + // Make sure intersections are NOT found between non-overlapping sub-arcs of the same circle. + EllipticalArc const arc1{Point(1, 0), Point(1, 1), 0, true, true, Point(0, 1)}; + EllipticalArc const arc2{Point(-1, 0), Point(1, 1), 0, true, true, Point(0, -1)}; + EXPECT_EQ(arc1.intersect(arc2).size(), 0u); + + // Overlapping sub-arcs but on an Ellipse with different rays. + EllipticalArc const eccentric{Point(2, 0), Point(2, 1), 0, true, true, Point(-2, 0)}; + EllipticalArc const subarc{eccentric.pointAtAngle(0.8), Point(2, 1), 0, true, true, eccentric.pointAtAngle(2)}; + EXPECT_EQ(eccentric.intersect(subarc).size(), 2u); + + // Check intersection times for two touching arcs. + EllipticalArc const lower{Point(-1, 0), Point(1, 1), 0, false, true, Point(0, -1)}; + auto expected_neg_x = upper.intersect(lower); + ASSERT_EQ(expected_neg_x.size(), 1); + auto const &left_pt = expected_neg_x[0]; + EXPECT_EQ(left_pt.point(), Point(-1, 0)); + EXPECT_DOUBLE_EQ(left_pt.first, 1.0); // Expect (-1, 0) reached at the end of upper + EXPECT_DOUBLE_EQ(left_pt.second, 0.0); // Expect (-1, 0) passed at the start of lower +} + +TEST(EllipticalArcTest, BezierIntersection) { + std::vector<CurveIntersection> r1, r2; + + EllipticalArc a3(Point(0,0), Point(1.5,5), M_PI/2, true, true, Point(0,2)); + CubicBezier bez1(Point(0,3), Point(7,3), Point(0,-1), Point(7,-1)); + r1 = a3.intersect(bez1); + EXPECT_EQ(r1.size(), 2u); + EXPECT_intersections_valid(a3, bez1, r1, 1e-10); + + EllipticalArc a4(Point(3,5), Point(5,1.5), 3*M_PI/2, true, true, Point(5,5)); + CubicBezier bez2(Point(0,5), Point(10,-4), Point(10,5), Point(0,-4)); + r2 = a4.intersect(bez2); + EXPECT_EQ(r2.size(), 4u); + EXPECT_intersections_valid(a4, bez2, r2, 1e-10); +} + +TEST(EllipticalArcTest, ExpandToTransformedTest) +{ + auto test_curve = [] (EllipticalArc const &c) { + constexpr int N = 50; + for (int i = 0; i < N; i++) { + auto angle = 2 * M_PI * i / N; + auto transform = Affine(Rotate(angle)) * Scale(0.9, 1.2); + + auto copy = std::unique_ptr<Curve>(c.duplicate()); + *copy *= transform; + auto box1 = copy->boundsExact(); + + auto pt = c.initialPoint() * transform; + auto box2 = Rect(pt, pt); + c.expandToTransformed(box2, transform); + + for (auto i : { X, Y }) { + EXPECT_NEAR(box1[i].min(), box2[i].min(), 2e-15); + EXPECT_NEAR(box1[i].max(), box2[i].max(), 2e-15); + } + } + }; + + test_curve(EllipticalArc(Point(0, 0), 1.0, 2.0, 0.0, false, false, Point(1, 1))); + test_curve(EllipticalArc(Point(0, 0), 3.0, 2.0, M_PI / 6, false, false, Point(1, 1))); + test_curve(EllipticalArc(Point(0, 0), 1.0, 2.0, M_PI / 5, true, true, Point(1, 1))); + test_curve(EllipticalArc(Point(1, 0), 1.0, 0.0, M_PI / 5, false, false, Point(1, 1))); + test_curve(EllipticalArc(Point(1, 0), 0.0, 0.0, 0.0, false, false, Point(2, 0))); + test_curve(EllipticalArc(Point(1, 0), 0.0, 0.0, 0.0, false, false, Point(1, 0))); +} diff --git a/tests/implicitization-test.cpp b/tests/implicitization-test.cpp new file mode 100644 index 0000000..bfc4c58 --- /dev/null +++ b/tests/implicitization-test.cpp @@ -0,0 +1,130 @@ +/* + * Test program for implicitization routines + * + * Authors: + * Marco Cecchetti <mrcekets at gmail.com> + * + * Copyright 2008 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/symbolic/implicit.h> + +#include "pick.h" + +#include <iostream> + + + + +void print_basis(Geom::SL::basis_type const& b) +{ + for (size_t i= 0; i < 2; ++i) + { + for (size_t j= 0; j < 3; ++j) + { + std::cout << "b[" << i << "][" << j << "] = " << b[i][j] << "\n"; + } + } +} + + + + +int main( int argc, char * argv[] ) +{ + // degree of polinomial parametrization + // warning: not set N to a value greater than 20! + // (10 in case you don't utilize the micro-basis) + // determinant computation becomes very expensive + unsigned int N = 4; + // max modulus of polynomial coefficients + unsigned int M = 1000; + + if (argc > 1) + N = std::atoi(argv[1]); + if (argc > 2) + M = std::atoi(argv[2]); + + Geom::SL::MVPoly1 f, g; + Geom::SL::basis_type b; + Geom::SL::MVPoly3 p, q; + Geom::SL::Matrix<Geom::SL::MVPoly2> B; + Geom::SL::MVPoly2 r; + + // generate two univariate polynomial with degree N + // and coeffcient in the range [-M, M] + f = pick_multipoly_max<1>(N, M); + g = pick_multipoly_max<1>(N, M); + + std::cout << "parametrization: \n"; + std::cout << "f = " << f << std::endl; + std::cout << "g = " << g << "\n\n"; + + // computes the micro-basis + microbasis(b, f, g); + // in case you want utilize directly the initial basis + // you should uncomment the next row and comment + // the microbasis function call + //make_initial_basis(b, f, g); + + std::cout << "generators in vector form : \n"; + print_basis(b); + std::cout << std::endl; + + // micro-basis generators + basis_to_poly(p, b[0]); + basis_to_poly(q, b[1]); + + std::cout << "generators as polynomial in R[t,x,y] : \n"; + std::cout << "p = " << p << std::endl; + std::cout << "q = " << q << "\n\n"; + + // make up the Bezout matrix and compute the determinant + B = make_bezout_matrix(p, q); + r = determinant_minor(B); + r.normalize(); + + std::cout << "Bezout matrix: (entries are bivariate polynomials) \n"; + std::cout << "B = " << B << "\n\n"; + std::cout << "determinant: \n"; + std::cout << "r(x, y) = " << r << "\n\n"; + + return EXIT_SUCCESS; +} + + +/* + 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 : diff --git a/tests/intersection-graph-test.cpp b/tests/intersection-graph-test.cpp new file mode 100644 index 0000000..19fb25c --- /dev/null +++ b/tests/intersection-graph-test.cpp @@ -0,0 +1,266 @@ +/** @file + * @brief Unit tests for PathIntersectionGraph, aka Boolean operations. + * Uses the Google Testing Framework + *//* + * 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 "testing.h" +#include <iostream> + +#include <2geom/intersection-graph.h> +#include <2geom/pathvector.h> +#include <2geom/svg-path-parser.h> +#include <2geom/svg-path-writer.h> +#include <glib.h> + +using namespace std; +using namespace Geom; + +Path string_to_path(const char* s) { + PathVector pv = parse_svg_path(s); + assert(pv.size() == 1u); + return pv[0]; +} + +enum Operation { + UNION, + INTERSECTION, + XOR, + A_MINUS_B, + B_MINUS_A +}; + +class IntersectionGraphTest : public ::testing::Test { +protected: + IntersectionGraphTest() { + rectangle = string_to_path("M 0,0 L 5,0 5,8 0,8 Z"); + bigrect = string_to_path("M -3,-4 L 7,-4 7,12 -3,12 Z"); + bigh = string_to_path("M 2,-3 L 3,-2 1,2 3,4 4,2 6,3 2,11 0,10 2,5 1,4 -1,6 -2,5 Z"); + smallrect = string_to_path("M 7,4 L 9,4 9,7 7,7 Z"); + g_random_set_seed(2345); + } + + void checkRandomPoints(PathVector const &a, PathVector const &b, PathVector const &result, + Operation op, unsigned npts = 5000) + { + Rect bounds = *(a.boundsFast() | b.boundsFast()); + for (unsigned i = 0; i < npts; ++i) { + Point p; + p[X] = g_random_double_range(bounds[X].min(), bounds[X].max()); + p[Y] = g_random_double_range(bounds[Y].min(), bounds[Y].max()); + bool in_a = a.winding(p) % 2; + bool in_b = b.winding(p) % 2; + bool in_res = result.winding(p) % 2; + + switch (op) { + case UNION: + EXPECT_EQ(in_res, in_a || in_b); + break; + case INTERSECTION: + EXPECT_EQ(in_res, in_a && in_b); + break; + case XOR: + EXPECT_EQ(in_res, in_a ^ in_b); + break; + case A_MINUS_B: + EXPECT_EQ(in_res, in_a && !in_b); + break; + case B_MINUS_A: + EXPECT_EQ(in_res, !in_a && in_b); + break; + } + } + } + + Path rectangle, bigrect, bigh, smallrect; +}; + +TEST_F(IntersectionGraphTest, Union) { + PathIntersectionGraph graph(rectangle, bigh); + //std::cout << graph << std::endl; + PathVector r = graph.getUnion(); + EXPECT_EQ(r.size(), 1u); + EXPECT_EQ(r.curveCount(), 19u); + + checkRandomPoints(rectangle, bigh, r, UNION); + + /*SVGPathWriter wr; + wr.feed(r); + std::cout << wr.str() << std::endl;*/ +} + +TEST_F(IntersectionGraphTest, DisjointUnion) { + PathIntersectionGraph graph(rectangle, smallrect); + + PathVector r = graph.getUnion(); + EXPECT_EQ(r.size(), 2u); + checkRandomPoints(rectangle, smallrect, r, UNION); +} + +TEST_F(IntersectionGraphTest, CoverUnion) { + PathIntersectionGraph graph(bigrect, bigh); + PathVector r = graph.getUnion(); + EXPECT_EQ(r.size(), 1u); + EXPECT_EQ(r, bigrect); +} + +TEST_F(IntersectionGraphTest, Subtraction) { + PathIntersectionGraph graph(rectangle, bigh); + PathVector a = graph.getAminusB(); + EXPECT_EQ(a.size(), 4u); + EXPECT_EQ(a.curveCount(), 17u); + checkRandomPoints(rectangle, bigh, a, A_MINUS_B); + + PathVector b = graph.getBminusA(); + EXPECT_EQ(b.size(), 4u); + EXPECT_EQ(b.curveCount(), 15u); + checkRandomPoints(rectangle, bigh, b, B_MINUS_A); + + PathVector x = graph.getXOR(); + EXPECT_EQ(x.size(), 8u); + EXPECT_EQ(x.curveCount(), 32u); + checkRandomPoints(rectangle, bigh, x, XOR); +} + +TEST_F(IntersectionGraphTest, PointOnEdge) { + PathVector a = string_to_path("M 0,0 L 10,0 10,10 0,10 z"); + PathVector b = string_to_path("M -5,2 L 0,2 5,5 0,8 -5,8 z"); + + PathIntersectionGraph graph(a, b); + PathVector u = graph.getUnion(); + //std::cout << u << std::endl; + EXPECT_EQ(u.size(), 1u); + EXPECT_EQ(u.curveCount(), 8u); + checkRandomPoints(a, b, u, UNION); + + PathVector i = graph.getIntersection(); + //std::cout << i << std::endl; + EXPECT_EQ(i.size(), 1u); + EXPECT_EQ(i.curveCount(), 3u); + checkRandomPoints(a, b, i, INTERSECTION); + + PathVector s1 = graph.getAminusB(); + //std::cout << s1 << std::endl; + EXPECT_EQ(s1.size(), 1u); + EXPECT_EQ(s1.curveCount(), 7u); + checkRandomPoints(a, b, s1, A_MINUS_B); + + PathVector s2 = graph.getBminusA(); + //std::cout << s2 << std::endl; + EXPECT_EQ(s2.size(), 1u); + EXPECT_EQ(s2.curveCount(), 4u); + checkRandomPoints(a, b, s2, B_MINUS_A); + + PathVector x = graph.getXOR(); + //std::cout << x << std::endl; + EXPECT_EQ(x.size(), 2u); + EXPECT_EQ(x.curveCount(), 11u); + checkRandomPoints(a, b, x, XOR); +} + +TEST_F(IntersectionGraphTest, RhombusInSquare) { + PathVector square = string_to_path("M 0,0 L 10,0 10,10 0,10 z"); + PathVector rhombus = string_to_path("M 5,0 L 10,5 5,10 0,5 z"); + + PathIntersectionGraph graph(square, rhombus); + //std::cout << graph << std::endl; + PathVector u = graph.getUnion(); + EXPECT_EQ(u.size(), 1u); + EXPECT_EQ(u.curveCount(), 4u); + checkRandomPoints(square, rhombus, u, UNION); + + PathVector i = graph.getIntersection(); + EXPECT_EQ(i.size(), 1u); + EXPECT_EQ(i.curveCount(), 4u); + checkRandomPoints(square, rhombus, i, INTERSECTION); + + PathVector s1 = graph.getAminusB(); + EXPECT_EQ(s1.size(), 2u); + EXPECT_EQ(s1.curveCount(), 8u); + checkRandomPoints(square, rhombus, s1, A_MINUS_B); + + PathVector s2 = graph.getBminusA(); + EXPECT_EQ(s2.size(), 0u); + EXPECT_EQ(s2.curveCount(), 0u); + checkRandomPoints(square, rhombus, s2, B_MINUS_A); +} + +TEST_F(IntersectionGraphTest, EmptyOperand) { + PathVector square = string_to_path("M 0,0 L 20, 0 L 20, 20 L 0, 20 Z"); + PathVector empty; + + auto graph = PathIntersectionGraph(square, empty); + // Taking union with the empty set should be a no-op: A ∪ ∅ = A + PathVector u = graph.getUnion(); + EXPECT_EQ(u.size(), 1u); + EXPECT_EQ(u.curveCount(), 4u); + + // Intersection with empty should produce empty: A ∩ ∅ = ∅ + PathVector i = graph.getIntersection(); + EXPECT_EQ(i.size(), 0u); + + // Subtracting empty set should be a no-op: A ∖ ∅ = A + PathVector rd = graph.getAminusB(); + EXPECT_EQ(rd.size(), 1u); + EXPECT_EQ(rd.curveCount(), 4u); + + // Subtracting FROM the empty set should produce the empty set: ∅ ∖ A = ∅ + PathVector ld = graph.getBminusA(); + EXPECT_EQ(ld.size(), 0u); +} + +// this test is disabled, since we cannot handle overlapping segments for now. +#if 0 +TEST_F(IntersectionGraphTest, EqualUnionAndIntersection) { + PathVector shape = string_to_path("M 0,0 L 2,1 -1,2 -1,3 0,3 z"); + PathIntersectionGraph graph(shape, shape); + std::cout << graph << std::endl; + PathVector a = graph.getUnion(); + std::cout << shape << std::endl; + std::cout << a << std::endl; + checkRandomPoints(shape, shape, a, UNION); + + PathIntersectionGraph graph2(bigh, bigh); + PathVector b = graph2.getIntersection(); + checkRandomPoints(bigh, bigh, b, INTERSECTION); + std::cout << b <<std::endl; +} +#endif + +/* + 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 : diff --git a/tests/interval-test.cpp b/tests/interval-test.cpp new file mode 100644 index 0000000..eccea70 --- /dev/null +++ b/tests/interval-test.cpp @@ -0,0 +1,54 @@ +/** @file + * @brief Unit tests for Interval, OptInterval, IntInterval, OptIntInterval. + *//* + * Authors: + * Thomas Holder + * + * Copyright 2021 Authors + * + * SPDX-License-Identifier: LGPL-2.1 OR MPL-1.1 + */ + +#include <2geom/interval.h> +#include <gtest/gtest.h> + +TEST(IntervalTest, EqualityTest) +{ + Geom::Interval a(3, 5), a2(a), b(4, 7); + Geom::OptInterval empty, oa = a; + + EXPECT_TRUE(a == a); + EXPECT_FALSE(a != a); + EXPECT_TRUE(a == a2); + EXPECT_FALSE(a != a2); + EXPECT_TRUE(empty == empty); + EXPECT_FALSE(empty != empty); + EXPECT_FALSE(a == empty); + EXPECT_TRUE(a != empty); + EXPECT_FALSE(empty == a); + EXPECT_TRUE(empty != a); + EXPECT_FALSE(a == b); + EXPECT_TRUE(a != b); + EXPECT_TRUE(a == oa); + EXPECT_FALSE(a != oa); + + Geom::IntInterval ia(3, 5), ia2(ia), ib(4, 7); + Geom::OptIntInterval iempty, ioa = ia; + + EXPECT_TRUE(ia == ia); + EXPECT_FALSE(ia != ia); + EXPECT_TRUE(ia == ia2); + EXPECT_FALSE(ia != ia2); + EXPECT_TRUE(iempty == iempty); + EXPECT_FALSE(iempty != iempty); + EXPECT_FALSE(ia == iempty); + EXPECT_TRUE(ia != iempty); + EXPECT_FALSE(iempty == ia); + EXPECT_TRUE(iempty != ia); + EXPECT_FALSE(ia == ib); + EXPECT_TRUE(ia != ib); + EXPECT_TRUE(ia == ioa); + EXPECT_FALSE(ia != ioa); +} + +// vim: filetype=cpp:expandtab:shiftwidth=4:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/tests/linalg-test.cpp b/tests/linalg-test.cpp new file mode 100644 index 0000000..b7e2f42 --- /dev/null +++ b/tests/linalg-test.cpp @@ -0,0 +1,502 @@ + + +#include "numeric/vector.h" +#include "numeric/matrix.h" + +#include <iostream> + +using namespace Geom; + + +template< class charT > +inline +std::basic_ostream<charT> & +operator<< (std::basic_ostream<charT> & os, const std::pair<size_t, size_t>& index_pair) +{ + os << "{" << index_pair.first << "," << index_pair.second << "}"; + return os; +} + +template< typename T, typename U> +void check_test( const char* description, T output, U expected ) +{ + bool result = ( output == expected ); + std::cout << "# " << description << " : "; + if ( result ) + std::cout << "success!" << std::endl; + else + std::cout << "fail!" << std::endl + << " output: " << output << std::endl + << " expected: " << expected << std::endl; +} + + +void vector_test() +{ + // Deprecated. Replaced by ** Vector examples ** + // in nl-vector-test.cpp + /* + NL::Vector v1(10), v2(10), v3(5); + for (unsigned int i = 0; i < v1.size(); ++i) + { + v1[i] = i; + } + std::cout << "v1: " << v1 << std::endl; + v2 = v1; + std::cout << "v2 = v1 : " << v2 << std::endl; + bool value = (v1 == v2); + std::cout << "(v1 == v2) : " << value << std::endl; + v2.scale(10); + std::cout << "v2.scale(10) : " << v2 << std::endl; + value = (v1 == v2); + std::cout << "(v1 == v2) : " << value << std::endl; + v2.translate(20); + std::cout << "v2.translate(20) : " << v2 << std::endl; + v2 += v1; + std::cout << "v2 += v1 : " << v2 << std::endl; + v2.swap_elements(3, 9); + std::cout << "v2.swap_elements(3, 9) : " << v2 << std::endl; + v2.reverse(); + std::cout << "v2.reverse() : " << v2 << std::endl; + value = v2.is_positive(); + std::cout << "v2.is_positive() : " << value << std::endl; + v2 -= v1; + std::cout << "v2 -= v1 : " << v2 << std::endl; + double bound = v2.max(); + std::cout << "v2.max() : " << bound << std::endl; + bound = v2.min(); + std::cout << "v2.min() : " << bound << std::endl; + unsigned int index = v2.max_index(); + std::cout << "v2.max_index() : " << index << std::endl; + index = v2.min_index(); + std::cout << "v2.min_index() : " << index << std::endl; + v2.set_basis(4); + std::cout << "v2.set_basis(4) : " << v2 << std::endl; + value = v2.is_non_negative(); + std::cout << "v2.is_non_negative() : " << value << std::endl; + v2.set_all(0); + std::cout << "v2.set_all(0) : " << v2 << std::endl; + value = v2.is_zero(); + std::cout << "v2.is_zero() : " << value << std::endl; + NL::swap(v1, v2); + std::cout << "swap(v1, v2) : v1: " << v1 << " v2: " << v2 << std::endl; + */ +} + + +void const_vector_view_test() +{ + NL::Vector v1(10); + for (unsigned int i = 0; i < v1.size(); ++i) + v1[i] = i; + NL::VectorView vv1(v1, 5, 1, 2); + vv1.scale(10); + std::cout << "v1 = " << v1 << std::endl; + + NL::ConstVectorView cvv1(v1, 6, 1); + check_test( "cvv1(v1, 6, 1)", cvv1.str(), "[10, 2, 30, 4, 50, 6]"); + NL::ConstVectorView cvv2(v1, 3, 1, 3); + check_test( "cvv2(v1, 3, 1, 3)", cvv2.str(), "[10, 4, 70]"); + NL::ConstVectorView cvv3(vv1, 3, 0, 2); + std::cout << "vv1 = " << vv1 << std::endl; + check_test( "cvv3(vv1, 3, 0, 2)", cvv3.str(), "[10, 50, 90]"); + NL::ConstVectorView cvv4(cvv1, 3, 1, 2); + check_test( "cvv4(cvv1, 3, 1, 2)", cvv4.str(), "[2, 4, 6]"); + bool value = (cvv2 == cvv4); + check_test( "(cvv2 == cvv4)", value, false); + + value = cvv2.is_zero(); + check_test( "cvv2.is_zero()", value, false); + value = cvv2.is_negative(); + check_test( "cvv2.is_negative()", value, false); + value = cvv2.is_positive(); + check_test( "cvv2.is_positive()", value, true); + value = cvv2.is_non_negative(); + check_test( "cvv2.is_non_negative()", value, true); + + NL::VectorView vv2(v1, 3, 1, 3); + vv2.scale(-1); + value = cvv2.is_zero(); + std::cout << "v1 = " << v1 << std::endl; + check_test( "cvv2.is_zero()", value, false); + value = cvv2.is_negative(); + check_test( "cvv2.is_negative()", value, true); + value = cvv2.is_positive(); + check_test( "cvv2.is_positive()", value, false); + value = cvv2.is_non_negative(); + check_test( "cvv2.is_non_negative()", value, false); + + vv2.set_all(0); + std::cout << "v1 = " << v1 << std::endl; + value = cvv2.is_zero(); + check_test( "cvv2.is_zero()", value, true); + value = cvv2.is_negative(); + check_test( "cvv2.is_negative()", value, false); + value = cvv2.is_positive(); + check_test( "cvv2.is_positive()", value, false); + value = cvv2.is_non_negative(); + check_test( "cvv2.is_non_negative()", value, true); + + vv1.reverse(); + vv2[0] = -1; + std::cout << "v1 = " << v1 << std::endl; + value = cvv2.is_zero(); + check_test( "cvv2.is_zero()", value, false); + value = cvv2.is_negative(); + check_test( "cvv2.is_negative()", value, false); + value = cvv2.is_positive(); + check_test( "cvv2.is_positive()", value, false); + value = cvv2.is_non_negative(); + check_test( "cvv2.is_non_negative()", value, false); + + vv2 = cvv2; + value = (vv2 == cvv2); + std::cout << "vv2 = " << vv2 << std::endl; + check_test( "(vv2 == cvv2)", value, true); + NL::Vector v2(cvv2.size()); + v2 = cvv4; + value = (v2 == cvv2); + std::cout << "v2 = " << v2 << std::endl; + check_test( "(v2 == cvv2)", value, false); + const NL::Vector v3(cvv2.size()); + NL::ConstVectorView cvv5(v3, v3.size()); + check_test( "cvv5(v3, v3.size())", cvv4.str(), "[2, 0, 6]"); + +} + +void vector_view_test() +{ + // Deprecated. Replaced by ** VectorView examples ** + // in nl-vector-test.cpp + /* + NL::Vector v1(10); + for (unsigned int i = 0; i < v1.size(); ++i) + v1[i] = i; + NL::VectorView vv1(v1, 5), vv2(v1, 5, 3), vv3(v1, 5, 0, 2), vv4(v1, 5, 1, 2); + std::cout << "v1 = " << v1 << std::endl; + check_test( "vv1(v1, 5)", vv1.str(), "[0, 1, 2, 3, 4]"); + check_test( "vv2(v1, 5, 3)", vv2.str(), "[3, 4, 5, 6, 7]"); + check_test( "vv3(v1, 5, 0, 2)", vv3.str(), "[0, 2, 4, 6, 8]"); + check_test( "vv4(v1, 5, 1, 2)", vv4.str(), "[1, 3, 5, 7, 9]"); + + NL::VectorView vv5(vv4, 3, 0, 2); + std::cout << "vv4 = " << vv4 << std::endl; + check_test( "vv5(vv4, 3, 0, 2)", vv5.str(), "[1, 5, 9]"); + vv5.scale(10); + check_test( "vv5.scale(10) : vv5", vv5.str(), "[10, 50, 90]"); + check_test( " : v1", v1.str(), "[0, 10, 2, 3, 4, 50, 6, 7, 8, 90]"); + vv5.translate(20); + check_test( "vv5.translate(20) : vv5", vv5.str(), "[30, 70, 110]"); + check_test( " : v1", v1.str(), "[0, 30, 2, 3, 4, 70, 6, 7, 8, 110]"); + vv1 += vv2; + check_test("vv1 += vv2", vv1.str(), "[3, 34, 72, 9, 11]"); + vv1 -= vv2; + check_test("vv1 -= vv2", vv1.str(), "[-6, 23, 2, 3, 4]"); + NL::ConstVectorView cvv1(vv3, 3); + vv5 = cvv1; + check_test("vv5 = cvv1", vv5.str(), "[-6, 2, 4]"); + vv5 += cvv1; + check_test("vv5 += cvv1", vv5.str(), "[-12, 4, 8]"); + vv5 -= cvv1; + check_test("vv5 -= cvv1", vv5.str(), "[-6, 2, 4]"); + NL::Vector v2(vv1); + std::cout << "v2 = " << v2 << std::endl; + vv1 = v2; + check_test( "vv1 = v2", vv1.str(), "[-6, -6, 2, 3, 4]"); + vv1 += v2; + check_test( "vv1 += v2", vv1.str(), "[-12, -12, 4, 6, 8]"); + vv1 -= v2; + check_test( "vv1 -= v2", vv1.str(), "[-6, -6, 2, 3, 4]"); + NL::swap_view(vv1, vv4); + check_test( "swap_view(vv1, vv4)", v1.str(), "[-6, -6, 2, 3, 4, 2, 6, 7, 8, 4]"); + */ +} + + +void const_matrix_view_test() +{ + NL::Matrix m0(8,4); + for (size_t i = 0; i < m0.rows(); ++i) + { + for (size_t j = 0; j < m0.columns(); ++j) + { + m0(i,j) = 10 * i + j; + } + } + std::cout << "m0 = " << m0 << std::endl; + + // constructor test + NL::Matrix m1(m0); + NL::ConstMatrixView cmv1(m1, 2, 1, 4, 2); + check_test("cmv1(m1, 2, 1, 4, 2)", cmv1.str(), "[[21, 22], [31, 32], [41, 42], [51, 52]]"); + NL::MatrixView mv1(m1, 2, 0, 4, 4); + NL::ConstMatrixView cmv2(mv1, 2, 1, 2, 2); + check_test("cmv2(mv1, 2, 1, 2, 2)", cmv2.str(), "[[41, 42], [51, 52]]"); + NL::ConstMatrixView cmv3(cmv1, 1, 1, 3, 1); + check_test("cmv3(cmv1, 1, 1, 2, 1)", cmv3.str(), "[[32], [42], [52]]"); + const NL::Matrix & m2 = m1; + NL::ConstMatrixView cmv4(m2, 2, 1, 4, 2); + check_test("cmv4(m2, 2, 1, 4, 2)", cmv4.str(), "[[21, 22], [31, 32], [41, 42], [51, 52]]"); + const NL::MatrixView & mv2 = mv1; + NL::ConstMatrixView cmv5(mv2, 2, 1, 2, 2); + check_test("cmv5(mv2, 2, 1, 2, 2)", cmv5.str(), "[[41, 42], [51, 52]]"); + + // row and column view test + NL::ConstVectorView cvv1 = cmv1.row_const_view(2); + check_test("cvv1 = cmv1.row_const_view(2)", cvv1.str(), "[41, 42]"); + NL::ConstVectorView cvv2 = cmv1.column_const_view(0); + check_test("cvv2 = cmv1.column_const_view(0)", cvv2.str(), "[21, 31, 41, 51]"); + + // property test + bool value = cmv1.is_negative(); + check_test("cmv1.is_negative()", value, false); + value = cmv1.is_non_negative(); + check_test("cmv1.is_non_negative()", value, true); + value = cmv1.is_positive(); + check_test("cmv1.is_positive()", value, true); + value = cmv1.is_zero(); + check_test("cmv1.is_zero()", value, false); + + m1.scale(-1); + value = cmv1.is_negative(); + check_test("cmv1.is_negative()", value, true); + value = cmv1.is_non_negative(); + check_test("cmv1.is_non_negative()", value, false); + value = cmv1.is_positive(); + check_test("cmv1.is_positive()", value, false); + value = cmv1.is_zero(); + check_test("cmv1.is_zero()", value, false); + + m1.translate(35); + value = cmv1.is_negative(); + check_test("cmv1.is_negative()", value, false); + value = cmv1.is_non_negative(); + check_test("cmv1.is_non_negative()", value, false); + value = cmv1.is_positive(); + check_test("cmv1.is_positive()", value, false); + value = cmv1.is_zero(); + check_test("cmv1.is_zero()", value, false); + + m1.set_all(0); + value = cmv1.is_negative(); + check_test("cmv1.is_negative()", value, false); + value = cmv1.is_non_negative(); + check_test("cmv1.is_non_negative()", value, true); + value = cmv1.is_positive(); + check_test("cmv1.is_positive()", value, false); + value = cmv1.is_zero(); + check_test("cmv1.is_zero()", value, true); + + m1.set_identity(); + value = cmv1.is_negative(); + check_test("cmv1.is_negative()", value, false); + value = cmv1.is_non_negative(); + check_test("cmv1.is_non_negative()", value, true); + value = cmv1.is_positive(); + check_test("cmv1.is_positive()", value, false); + value = cmv1.is_zero(); + check_test("cmv1.is_zero()", value, false); + + // max, min test + m1 = m0; + std::cout << "cmv1 = " << cmv1 << std::endl; + std::pair<size_t, size_t> out_elem = cmv1.max_index(); + std::pair<size_t, size_t> exp_elem(3,1); + check_test("cmv1.max_index()", out_elem, exp_elem); + double bound = cmv1.max(); + check_test("cmv1.max()", bound, cmv1(exp_elem.first, exp_elem.second)); + out_elem = cmv1.min_index(); + exp_elem.first = 0; exp_elem.second = 0; + check_test("cmv1.min_index()", out_elem, exp_elem); + bound = cmv1.min(); + check_test("cmv1.min()", bound, cmv1(exp_elem.first, exp_elem.second)); + +} + + +void matrix_view_test() +{ + NL::Matrix m0(8,4); + for (size_t i = 0; i < m0.rows(); ++i) + { + for (size_t j = 0; j < m0.columns(); ++j) + { + m0(i,j) = 10 * i + j; + } + } + std::cout << "m0 = " << m0 << std::endl; + + // constructor test + NL::Matrix m1(m0); + NL::MatrixView mv1(m1, 2, 1, 4, 2); + check_test("mv1(m1, 2, 1, 4, 2)", mv1.str(), "[[21, 22], [31, 32], [41, 42], [51, 52]]"); + NL::MatrixView mv2(mv1, 2, 1, 2, 1); + check_test("mv2(mv1, 2, 1, 2, 1)", mv2.str(), "[[42], [52]]"); + + // operator = test + NL::Matrix m2(4,2); + m2.set_all(0); + mv1 = m2; + check_test("mv1 = m2", m1.str(), "[[0, 1, 2, 3], [10, 11, 12, 13], [20, 0, 0, 23], [30, 0, 0, 33], [40, 0, 0, 43], [50, 0, 0, 53], [60, 61, 62, 63], [70, 71, 72, 73]]"); + m1 = m0; + NL::MatrixView mv3(m2, 0, 0, 4, 2); + mv1 = mv3; + check_test("mv1 = mv3", m1.str(), "[[0, 1, 2, 3], [10, 11, 12, 13], [20, 0, 0, 23], [30, 0, 0, 33], [40, 0, 0, 43], [50, 0, 0, 53], [60, 61, 62, 63], [70, 71, 72, 73]]"); + m1 = m0; + NL::ConstMatrixView cmv1(m2, 0, 0, 4, 2); + mv1 = cmv1; + check_test("mv1 = cmv1", m1.str(), "[[0, 1, 2, 3], [10, 11, 12, 13], [20, 0, 0, 23], [30, 0, 0, 33], [40, 0, 0, 43], [50, 0, 0, 53], [60, 61, 62, 63], [70, 71, 72, 73]]"); + + // operator == test + m2.set_identity(); + mv1 = m2; + bool value = (mv1 == m2); + check_test("(mv1 == m2)", value, true); + value = (mv1 == mv3); + check_test("(mv1 == mv3)", value, true); + value = (mv1 == cmv1); + check_test("(mv1 == cmv1)", value, true); + + // row and column view test + m1 = m0; + NL::ConstVectorView cvv1 = mv1.row_const_view(2); + check_test("cvv1 = mv1.row_const_view(2)", cvv1.str(), "[41, 42]"); + NL::ConstVectorView cvv2 = mv1.column_const_view(0); + check_test("cvv2 = mv1.column_const_view(0)", cvv2.str(), "[21, 31, 41, 51]"); + NL::VectorView vv1 = mv1.row_view(2); + check_test("vv1 = mv1.row_view(2)", vv1.str(), "[41, 42]"); + NL::VectorView vv2 = mv1.column_view(0); + check_test("vv2 = mv1.column_view(0)", vv2.str(), "[21, 31, 41, 51]"); + + // swap_view test + m1 = m0; + swap_view(mv1, mv3); + check_test("swap_view(mv1, mv3) : mv1", mv1.str(), "[[1, 0], [0, 1], [0, 0], [0, 0]]"); + check_test(" : m1", m1.str(), m0.str()); + check_test(" : mv3", mv3.str(), "[[21, 22], [31, 32], [41, 42], [51, 52]]"); + check_test(" : m2", m2.str(), "[[1, 0], [0, 1], [0, 0], [0, 0]]"); + swap_view(mv1, mv3); + + // modifying operations test + m1 = m0; + m2.set_all(10); + mv1 += m2; + check_test("mv1 += m2", m1.str(), "[[0, 1, 2, 3], [10, 11, 12, 13], [20, 31, 32, 23], [30, 41, 42, 33], [40, 51, 52, 43], [50, 61, 62, 53], [60, 61, 62, 63], [70, 71, 72, 73]]"); + mv1 -= m2; + check_test("mv1 -= m2", m1.str(), m0.str()); + mv1 += mv3; + check_test("mv1 += mv3", m1.str(), "[[0, 1, 2, 3], [10, 11, 12, 13], [20, 31, 32, 23], [30, 41, 42, 33], [40, 51, 52, 43], [50, 61, 62, 53], [60, 61, 62, 63], [70, 71, 72, 73]]"); + mv1 -= mv3; + check_test("mv1 -= mv3", m1.str(), m0.str()); + mv1 += cmv1; + check_test("mv1 += cmv1", m1.str(), "[[0, 1, 2, 3], [10, 11, 12, 13], [20, 31, 32, 23], [30, 41, 42, 33], [40, 51, 52, 43], [50, 61, 62, 53], [60, 61, 62, 63], [70, 71, 72, 73]]"); + mv1 -= cmv1; + check_test("mv1 -= cmv1", m1.str(), m0.str()); + + m1 = m0; + mv1.swap_rows(0,3); + check_test("mv1.swap_rows(0,3)", m1.str(), "[[0, 1, 2, 3], [10, 11, 12, 13], [20, 51, 52, 23], [30, 31, 32, 33], [40, 41, 42, 43], [50, 21, 22, 53], [60, 61, 62, 63], [70, 71, 72, 73]]"); + m1 = m0; + mv1.swap_columns(0,1); + check_test("mv1.swap_columns(0,3)", m1.str(), "[[0, 1, 2, 3], [10, 11, 12, 13], [20, 22, 21, 23], [30, 32, 31, 33], [40, 42, 41, 43], [50, 52, 51, 53], [60, 61, 62, 63], [70, 71, 72, 73]]"); + + m1 = m0; + NL::MatrixView mv4(m1, 0, 0, 4, 4); + mv4.transpose(); + check_test("mv4.transpose()", m1.str(), "[[0, 10, 20, 30], [1, 11, 21, 31], [2, 12, 22, 32], [3, 13, 23, 33], [40, 41, 42, 43], [50, 51, 52, 53], [60, 61, 62, 63], [70, 71, 72, 73]]"); + +} + +void matrix_test() +{ + NL::Matrix m0(8,4); + for (size_t i = 0; i < m0.rows(); ++i) + { + for (size_t j = 0; j < m0.columns(); ++j) + { + m0(i,j) = 10 * i + j; + } + } + std::cout << "m0 = " << m0 << std::endl; + + // constructor test + NL::Matrix m1(m0); + check_test("m1(m0)", m1.str(), m0.str()); + NL::MatrixView mv1(m0, 2, 1, 4, 2); + NL::Matrix m2(mv1); + check_test("m2(mv1)", m2.str(), mv1.str()); + NL::MatrixView cmv1(m0, 2, 1, 4, 2); + NL::Matrix m3(cmv1); + check_test("m3(cmv1)", m3.str(), cmv1.str()); + + // operator = and operator == test + m1.set_all(0); + m1 = m0; + check_test("m1 = m0", m1.str(), m0.str()); + bool value = (m1 == m0); + check_test("m1 == m0", value, true); + m2.set_all(0); + m2 = mv1; + check_test("m2 = mv1", m2.str(), mv1.str()); + value = (m2 == mv1); + check_test("m2 == mv1", value, true); + m2.set_all(0); + m2 = cmv1; + check_test("m2 = cmv1", m2.str(), cmv1.str()); + value = (m2 == cmv1); + check_test("m2 == cmv1", value, true); + + // row and column view test + NL::ConstVectorView cvv1 = m2.row_const_view(2); + check_test("cvv1 = m2.row_const_view(2)", cvv1.str(), "[41, 42]"); + NL::ConstVectorView cvv2 = m2.column_const_view(0); + check_test("cvv2 = m2.column_const_view(0)", cvv2.str(), "[21, 31, 41, 51]"); + NL::VectorView vv1 = m2.row_view(2); + check_test("vv1 = m2.row_view(2)", vv1.str(), "[41, 42]"); + NL::VectorView vv2 = m2.column_view(0); + check_test("vv2 = m2.column_view(0)", vv2.str(), "[21, 31, 41, 51]"); + + + // modifying operations test + NL::Matrix m4(8,4); + m4.set_all(0); + m1.set_all(0); + m1 += m0; + check_test("m1 += m0", m1.str(), m0.str()); + m1 -= m0; + check_test("m1 -= m0", m1.str(), m4.str()); + NL::Matrix m5(4,2); + m5.set_all(0); + m2.set_all(0); + m2 += mv1; + check_test("m2 += mv1", m2.str(), mv1.str()); + m2 -= mv1; + check_test("m2 -= mv1", m2.str(), m5.str()); + m2.set_all(0); + m2 += cmv1; + check_test("m2 += cmv1", m2.str(), cmv1.str()); + m2 -= cmv1; + check_test("m2 -= cmv1", m2.str(), m5.str()); + + // swap test + m3.set_identity(); + m5.set_identity(); + m1 = m0; + m2 = mv1; + swap(m2, m3); + check_test("swap(m2, m3) : m2", m2.str(), m5.str()); + check_test(" : m3", m3.str(), mv1.str()); + +} + +int main(int argc, char **argv) +{ + //const_vector_view_test(); + //vector_view_test(); + const_matrix_view_test(); + //matrix_view_test(); + //matrix_test(); + return 0; +} + + diff --git a/tests/line-test.cpp b/tests/line-test.cpp new file mode 100644 index 0000000..0625566 --- /dev/null +++ b/tests/line-test.cpp @@ -0,0 +1,185 @@ +/** @file + * @brief Unit tests for Line and related functions + * Uses the Google Testing Framework + *//* + * 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 "testing.h" +#include <iostream> +#include <glib.h> + +#include <2geom/line.h> +#include <2geom/affine.h> + +using namespace Geom; + +TEST(LineTest, VectorAndVersor) { + Line a(Point(10, 10), Point(-10, 20)); + Line b(Point(10, 10), Point(15, 15)); + EXPECT_EQ(a.vector(), Point(-20, 10)); + EXPECT_EQ(b.vector(), Point(5, 5)); + EXPECT_EQ(a.versor(), a.vector().normalized()); + EXPECT_EQ(b.versor(), b.vector().normalized()); +} + +TEST(LineTest, AngleBisector) { + Point o(0,0), a(1,1), b(3,0), c(-4, 0); + Point d(0.5231, 0.75223); + + // normal + Line ab1 = make_angle_bisector_line(a + d, o + d, b + d); + Line ab2 = make_angle_bisector_line(a - d, o - d, b - d); + EXPECT_FLOAT_EQ(ab1.angle(), Angle::from_degrees(22.5)); + EXPECT_FLOAT_EQ(ab2.angle(), Angle::from_degrees(22.5)); + + // half angle + Line bc1 = make_angle_bisector_line(b + d, o + d, c + d); + Line bc2 = make_angle_bisector_line(b - d, o - d, c - d); + EXPECT_FLOAT_EQ(bc1.angle(), Angle::from_degrees(90)); + EXPECT_FLOAT_EQ(bc2.angle(), Angle::from_degrees(90)); + + // zero angle + Line aa1 = make_angle_bisector_line(a + d, o + d, a + d); + Line aa2 = make_angle_bisector_line(a - d, o - d, a - d); + EXPECT_FLOAT_EQ(aa1.angle(), Angle::from_degrees(45)); + EXPECT_FLOAT_EQ(aa2.angle(), Angle::from_degrees(45)); +} + +TEST(LineTest, Equality) { + Line a(Point(0,0), Point(2,2)); + Line b(Point(2,2), Point(5,5)); + + EXPECT_EQ(a, a); + EXPECT_EQ(b, b); + EXPECT_EQ(a, b); +} + +TEST(LineTest, Reflection) { + Line a(Point(10, 0), Point(15,5)); + Point pa(10,5), ra(15,0); + + Line b(Point(1,-2), Point(2,0)); + Point pb(5,1), rb(1,3); + Affine reflecta = a.reflection(), reflectb = b.reflection(); + + Point testra = pa * reflecta; + Point testrb = pb * reflectb; + + constexpr Coord eps{1e-12}; + EXPECT_near(testra[X], ra[X], eps); + EXPECT_near(testra[Y], ra[Y], eps); + EXPECT_near(testrb[X], rb[X], eps); + EXPECT_near(testrb[Y], rb[Y], eps); +} + +TEST(LineTest, RotationToZero) { + Line a(Point(-5,23), Point(15,27)); + Affine mx = a.rotationToZero(X); + Affine my = a.rotationToZero(Y); + + for (unsigned i = 0; i <= 12; ++i) { + double t = -1 + 0.25 * i; + Point p = a.pointAt(t); + Point rx = p * mx; + Point ry = p * my; + //std::cout << rx[X] << " " << ry[Y] << std::endl; + // unfortunately this is precise only to about 1e-14 + EXPECT_NEAR(rx[X], 0, 1e-14); + EXPECT_NEAR(ry[Y], 0, 1e-14); + } +} + +TEST(LineTest, Coefficients) { + std::vector<Line> lines; + lines.emplace_back(Point(1e3,1e3), Point(1,1)); + //the case below will never work without normalizing the line + //lines.emplace_back(Point(1e5,1e5), Point(1e-15,0)); + lines.emplace_back(Point(1e5,1e5), Point(1e5,-1e5)); + lines.emplace_back(Point(-3,10), Point(3,10)); + lines.emplace_back(Point(250,333), Point(-72,121)); + + for (auto & line : lines) { + Coord a, b, c, A, B, C; + line.coefficients(a, b, c); + /*std::cout << format_coord_nice(a) << " " + << format_coord_nice(b) << " " + << format_coord_nice(c) << std::endl;*/ + Line k(a, b, c); + //std::cout << k.initialPoint() << " " << k.finalPoint() << std::endl; + k.coefficients(A, B, C); + /*std::cout << format_coord_nice(A) << " " + << format_coord_nice(B) << " " + << format_coord_nice(C) << std::endl;*/ + EXPECT_DOUBLE_EQ(a, A); + EXPECT_DOUBLE_EQ(b, B); + EXPECT_DOUBLE_EQ(c, C); + + for (unsigned j = 0; j <= 10; ++j) { + double t = j / 10.; + Point p = line.pointAt(t); + /*std::cout << t << " " << p << " " + << A*p[X] + B*p[Y] + C << " " + << A*(p[X]-1) + B*(p[Y]+1) + C << std::endl;*/ + EXPECT_near(A*p[X] + B*p[Y] + C, 0., 2e-11); + EXPECT_not_near(A*(p[X]-1) + B*(p[Y]+1) + C, 0., 1e-6); + } + } +} + +TEST(LineTest, Intersection) { + Line a(Point(0,3), Point(1,2)); + Line b(Point(0,-3), Point(1,-2)); + LineSegment lsa(Point(0,3), Point(1,2)); + LineSegment lsb(Point(0,-3), Point(1,-2)); + LineSegment lsc(Point(3,1), Point(3, -1)); + + std::vector<ShapeIntersection> r1, r2, r3; + + r1 = a.intersect(b); + ASSERT_EQ(r1.size(), 1u); + EXPECT_EQ(r1[0].point(), Point(3,0)); + EXPECT_intersections_valid(a, b, r1, 1e-15); + + r2 = a.intersect(lsc); + ASSERT_EQ(r2.size(), 1u); + EXPECT_EQ(r2[0].point(), Point(3,0)); + EXPECT_intersections_valid(a, lsc, r2, 1e-15); + + r3 = b.intersect(lsc); + ASSERT_EQ(r3.size(), 1u); + EXPECT_EQ(r3[0].point(), Point(3,0)); + EXPECT_intersections_valid(a, lsc, r3, 1e-15); + + EXPECT_TRUE(lsa.intersect(lsb).empty()); + EXPECT_TRUE(lsa.intersect(lsc).empty()); + EXPECT_TRUE(lsb.intersect(lsc).empty()); + EXPECT_TRUE(a.intersect(lsb).empty()); + EXPECT_TRUE(b.intersect(lsa).empty()); +} diff --git a/tests/mersennetwister.h b/tests/mersennetwister.h new file mode 100644 index 0000000..bc19d8e --- /dev/null +++ b/tests/mersennetwister.h @@ -0,0 +1,427 @@ +/** + * 2Geom developers: + * For licence reasons, Do not copy code from this header into other files + * */ +// MersenneTwister.h +// Mersenne Twister random number generator -- a C++ class MTRand +// Based on code by Makoto Matsumoto, Takuji Nishimura, and Shawn Cokus +// Richard J. Wagner v1.0 15 May 2003 rjwagner@writeme.com + +// The Mersenne Twister is an algorithm for generating random numbers. It +// was designed with consideration of the flaws in various other generators. +// The period, 2^19937-1, and the order of equidistribution, 623 dimensions, +// are far greater. The generator is also fast; it avoids multiplication and +// division, and it benefits from caches and pipelines. For more information +// see the inventors' web page at http://www.math.keio.ac.jp/~matumoto/emt.html + +// Reference +// M. Matsumoto and T. Nishimura, "Mersenne Twister: A 623-Dimensionally +// Equidistributed Uniform Pseudo-Random Number Generator", ACM Transactions on +// Modeling and Computer Simulation, Vol. 8, No. 1, January 1998, pp 3-30. + +// Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, +// Copyright (C) 2000 - 2003, Richard J. Wagner +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// 3. The names of its contributors may not be used to endorse or promote +// products derived from this software without specific prior written +// permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// The original code included the following notice: +// +// When you use this, send an email to: matumoto@math.keio.ac.jp +// with an appropriate reference to your work. +// +// It would be nice to CC: rjwagner@writeme.com and Cokus@math.washington.edu +// when you write. + +#ifndef MERSENNETWISTER_H +#define MERSENNETWISTER_H + +// Not thread safe (unless auto-initialization is avoided and each thread has +// its own MTRand object) + +#include <iostream> +#include <limits.h> +#include <stdio.h> +#include <time.h> +#include <math.h> + +class MTRand { + // Data + public: + typedef unsigned long uint32; // unsigned integer type, at least 32 bits + + enum { N = 624 }; // length of state vector + enum { SAVE = N + 1 }; // length of array for save() + + protected: + enum { M = 397 }; // period parameter + + uint32 state[N]; // internal state + uint32 *pNext; // next value to get from state + int left; // number of values left before reload needed + + + //Methods + public: + MTRand( const uint32& oneSeed ); // initialize with a simple uint32 + MTRand( uint32 *const bigSeed, uint32 const seedLength = N ); // or an array + MTRand(); // auto-initialize with /dev/urandom or time() and clock() + + // Do NOT use for CRYPTOGRAPHY without securely hashing several returned + // values together, otherwise the generator state can be learned after + // reading 624 consecutive values. + + // Access to 32-bit random numbers + double rand(); // real number in [0,1] + double rand( const double& n ); // real number in [0,n] + double randExc(); // real number in [0,1) + double randExc( const double& n ); // real number in [0,n) + double randDblExc(); // real number in (0,1) + double randDblExc( const double& n ); // real number in (0,n) + uint32 randInt(); // integer in [0,2^32-1] + uint32 randInt( const uint32& n ); // integer in [0,n] for n < 2^32 + double operator()() { return rand(); } // same as rand() + + // Access to 53-bit random numbers (capacity of IEEE double precision) + double rand53(); // real number in [0,1) + + // Access to nonuniform random number distributions + double randNorm( const double& mean = 0.0, const double& variance = 1.0 ); + + // Re-seeding functions with same behavior as initializers + void seed( const uint32 oneSeed ); + void seed( uint32 *const bigSeed, const uint32 seedLength = N ); + void seed(); + + // Saving and loading generator state + void save( uint32* saveArray ) const; // to array of size SAVE + void load( uint32 *const loadArray ); // from such array + friend std::ostream& operator<<( std::ostream& os, const MTRand& mtrand ); + friend std::istream& operator>>( std::istream& is, MTRand& mtrand ); + + protected: + void initialize( const uint32 oneSeed ); + void reload(); + uint32 hiBit( const uint32& u ) const { return u & 0x80000000UL; } + uint32 loBit( const uint32& u ) const { return u & 0x00000001UL; } + uint32 loBits( const uint32& u ) const { return u & 0x7fffffffUL; } + uint32 mixBits( const uint32& u, const uint32& v ) const + { return hiBit(u) | loBits(v); } + uint32 twist( const uint32& m, const uint32& s0, const uint32& s1 ) const + { return m ^ (mixBits(s0,s1)>>1) ^ (-loBit(s1) & 0x9908b0dfUL); } + static uint32 hash( time_t t, clock_t c ); +}; + + +inline MTRand::MTRand( const uint32& oneSeed ) +{ seed(oneSeed); } + +inline MTRand::MTRand( uint32 *const bigSeed, const uint32 seedLength ) +{ seed(bigSeed,seedLength); } + +inline MTRand::MTRand() +{ seed(); } + +inline double MTRand::rand() +{ return double(randInt()) * (1.0/4294967295.0); } + +inline double MTRand::rand( const double& n ) +{ return rand() * n; } + +inline double MTRand::randExc() +{ return double(randInt()) * (1.0/4294967296.0); } + +inline double MTRand::randExc( const double& n ) +{ return randExc() * n; } + +inline double MTRand::randDblExc() +{ return ( double(randInt()) + 0.5 ) * (1.0/4294967296.0); } + +inline double MTRand::randDblExc( const double& n ) +{ return randDblExc() * n; } + +inline double MTRand::rand53() +{ + uint32 a = randInt() >> 5, b = randInt() >> 6; + return ( a * 67108864.0 + b ) * (1.0/9007199254740992.0); // by Isaku Wada +} + +inline double MTRand::randNorm( const double& mean, const double& variance ) +{ + // Return a real number from a normal (Gaussian) distribution with given + // mean and variance by Box-Muller method + double r = sqrt( -2.0 * log( 1.0-randDblExc()) ) * variance; + double phi = 2.0 * 3.14159265358979323846264338328 * randExc(); + return mean + r * cos(phi); +} + +inline MTRand::uint32 MTRand::randInt() +{ + // Pull a 32-bit integer from the generator state + // Every other access function simply transforms the numbers extracted here + + if( left == 0 ) reload(); + --left; + + register uint32 s1; + s1 = *pNext++; + s1 ^= (s1 >> 11); + s1 ^= (s1 << 7) & 0x9d2c5680UL; + s1 ^= (s1 << 15) & 0xefc60000UL; + return ( s1 ^ (s1 >> 18) ); +} + +inline MTRand::uint32 MTRand::randInt( const uint32& n ) +{ + // Find which bits are used in n + // Optimized by Magnus Jonsson (magnus@smartelectronix.com) + uint32 used = n; + used |= used >> 1; + used |= used >> 2; + used |= used >> 4; + used |= used >> 8; + used |= used >> 16; + + // Draw numbers until one is found in [0,n] + uint32 i; + do + i = randInt() & used; // toss unused bits to shorten search + while( i > n ); + return i; +} + + +inline void MTRand::seed( const uint32 oneSeed ) +{ + // Seed the generator with a simple uint32 + initialize(oneSeed); + reload(); +} + + +inline void MTRand::seed( uint32 *const bigSeed, const uint32 seedLength ) +{ + // Seed the generator with an array of uint32's + // There are 2^19937-1 possible initial states. This function allows + // all of those to be accessed by providing at least 19937 bits (with a + // default seed length of N = 624 uint32's). Any bits above the lower 32 + // in each element are discarded. + // Just call seed() if you want to get array from /dev/urandom + initialize(19650218UL); + register int i = 1; + register uint32 j = 0; + register int k = ( uint32(N) > seedLength ? uint32(N) : seedLength ); + for( ; k; --k ) + { + state[i] = + state[i] ^ ( (state[i-1] ^ (state[i-1] >> 30)) * 1664525UL ); + state[i] += ( bigSeed[j] & 0xffffffffUL ) + j; + state[i] &= 0xffffffffUL; + ++i; ++j; + if( i >= N ) { state[0] = state[N-1]; i = 1; } + if( j >= seedLength ) j = 0; + } + for( k = N - 1; k; --k ) + { + state[i] = + state[i] ^ ( (state[i-1] ^ (state[i-1] >> 30)) * 1566083941UL ); + state[i] -= i; + state[i] &= 0xffffffffUL; + ++i; + if( i >= N ) { state[0] = state[N-1]; i = 1; } + } + state[0] = 0x80000000UL; // MSB is 1, assuring non-zero initial array + reload(); +} + + +inline void MTRand::seed() +{ + // Seed the generator with an array from /dev/urandom if available + // Otherwise use a hash of time() and clock() values + + // First try getting an array from /dev/urandom + FILE* urandom = fopen( "/dev/urandom", "rb" ); + if( urandom ) + { + uint32 bigSeed[N]; + register uint32 *s = bigSeed; + register int i = N; + register bool success = true; + while( success && i-- ) + success = fread( s++, sizeof(uint32), 1, urandom ); + fclose(urandom); + if( success ) { seed( bigSeed, N ); return; } + } + + // Was not successful, so use time() and clock() instead + seed( hash( time(NULL), clock() ) ); +} + + +inline void MTRand::initialize( const uint32 seed ) +{ + // Initialize generator state with seed + // See Knuth TAOCP Vol 2, 3rd Ed, p.106 for multiplier. + // In previous versions, most significant bits (MSBs) of the seed affect + // only MSBs of the state array. Modified 9 Jan 2002 by Makoto Matsumoto. + register uint32 *s = state; + register uint32 *r = state; + register int i = 1; + *s++ = seed & 0xffffffffUL; + for( ; i < N; ++i ) + { + *s++ = ( 1812433253UL * ( *r ^ (*r >> 30) ) + i ) & 0xffffffffUL; + r++; + } +} + + +inline void MTRand::reload() +{ + // Generate N new values in state + // Made clearer and faster by Matthew Bellew (matthew.bellew@home.com) + register uint32 *p = state; + register int i; + for( i = N - M; i--; ++p ) + *p = twist( p[M], p[0], p[1] ); + for( i = M; --i; ++p ) + *p = twist( p[M-N], p[0], p[1] ); + *p = twist( p[M-N], p[0], state[0] ); + + left = N, pNext = state; +} + + +inline MTRand::uint32 MTRand::hash( time_t t, clock_t c ) +{ + // Get a uint32 from t and c + // Better than uint32(x) in case x is floating point in [0,1] + // Based on code by Lawrence Kirby (fred@genesis.demon.co.uk) + + static uint32 differ = 0; // guarantee time-based seeds will change + + uint32 h1 = 0; + unsigned char *p = (unsigned char *) &t; + for( size_t i = 0; i < sizeof(t); ++i ) + { + h1 *= UCHAR_MAX + 2U; + h1 += p[i]; + } + uint32 h2 = 0; + p = (unsigned char *) &c; + for( size_t j = 0; j < sizeof(c); ++j ) + { + h2 *= UCHAR_MAX + 2U; + h2 += p[j]; + } + return ( h1 + differ++ ) ^ h2; +} + + +inline void MTRand::save( uint32* saveArray ) const +{ + register uint32 *sa = saveArray; + register const uint32 *s = state; + register int i = N; + for( ; i--; *sa++ = *s++ ) {} + *sa = left; +} + + +inline void MTRand::load( uint32 *const loadArray ) +{ + register uint32 *s = state; + register uint32 *la = loadArray; + register int i = N; + for( ; i--; *s++ = *la++ ) {} + left = *la; + pNext = &state[N-left]; +} + + +inline std::ostream& operator<<( std::ostream& os, const MTRand& mtrand ) +{ + register const MTRand::uint32 *s = mtrand.state; + register int i = mtrand.N; + for( ; i--; os << *s++ << "\t" ) {} + return os << mtrand.left; +} + + +inline std::istream& operator>>( std::istream& is, MTRand& mtrand ) +{ + register MTRand::uint32 *s = mtrand.state; + register int i = mtrand.N; + for( ; i--; is >> *s++ ) {} + is >> mtrand.left; + mtrand.pNext = &mtrand.state[mtrand.N-mtrand.left]; + return is; +} + +#endif // MERSENNETWISTER_H + +// Change log: +// +// v0.1 - First release on 15 May 2000 +// - Based on code by Makoto Matsumoto, Takuji Nishimura, and Shawn Cokus +// - Translated from C to C++ +// - Made completely ANSI compliant +// - Designed convenient interface for initialization, seeding, and +// obtaining numbers in default or user-defined ranges +// - Added automatic seeding from /dev/urandom or time() and clock() +// - Provided functions for saving and loading generator state +// +// v0.2 - Fixed bug which reloaded generator one step too late +// +// v0.3 - Switched to clearer, faster reload() code from Matthew Bellew +// +// v0.4 - Removed trailing newline in saved generator format to be consistent +// with output format of built-in types +// +// v0.5 - Improved portability by replacing static const int's with enum's and +// clarifying return values in seed(); suggested by Eric Heimburg +// - Removed MAXINT constant; use 0xffffffffUL instead +// +// v0.6 - Eliminated seed overflow when uint32 is larger than 32 bits +// - Changed integer [0,n] generator to give better uniformity +// +// v0.7 - Fixed operator precedence ambiguity in reload() +// - Added access for real numbers in (0,1) and (0,n) +// +// v0.8 - Included time.h header to properly support time_t and clock_t +// +// v1.0 - Revised seeding to match 26 Jan 2002 update of Nishimura and Matsumoto +// - Allowed for seeding with arrays of any length +// - Added access for real numbers in [0,1) with 53-bit resolution +// - Added access for real numbers from normal (Gaussian) distributions +// - Increased overall speed by optimizing twist() +// - Doubled speed of integer [0,n] generation +// - Fixed out-of-range number generation on 64-bit machines +// - Improved portability by substituting literal constants for long enum's +// - Changed license from GNU LGPL to BSD diff --git a/tests/nl-vector-test.cpp b/tests/nl-vector-test.cpp new file mode 100644 index 0000000..53e8eaa --- /dev/null +++ b/tests/nl-vector-test.cpp @@ -0,0 +1,333 @@ +/** @file + * @brief Unit tests for Vector, VectorView + *//* + * Authors: + * Olof Bjarnason <olof.bjarnason@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 <gtest/gtest.h> + +#include <2geom/numeric/vector.h> + +namespace Geom { + +//// +// Test fixture used in many tests. +// v1 = [0, 1, ..., 8, 9] +//// +class CountingVectorFixture : public ::testing::Test { +public: + CountingVectorFixture() : v1(10) { } + +protected: + void SetUp() override { + for (unsigned int i = 0; i < this->v1.size(); ++i) + this->v1[i] = i; + } + + NL::Vector v1; +}; + +// These types are only here to differentiate +// between categories of tests - they both use +// the same v1 test fixture variable. +class VectorTest : public CountingVectorFixture { }; +class VectorViewTest : public CountingVectorFixture { }; + +//// +// Helper method to write simple tests +//// +NL::Vector V3(double a, double b, double c) { + NL::Vector v(3); + v[0] = a; + v[1] = b; + v[2] = c; + return v; +} + +//// +// ** Vector examples ** +//// + +TEST_F(VectorTest, VectorStringRepresentation) { + EXPECT_EQ(v1.str(), "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"); +} + +TEST_F(VectorTest, VectorConstructFromAnother) { + NL::Vector v2(v1); + EXPECT_EQ(v1.str(), v2.str()); +} + +TEST_F(VectorTest, OperatorEqualIsDefined) { + EXPECT_TRUE(v1 == v1); + NL::Vector v2(v1); + EXPECT_TRUE(v1 == v2); + // TODO: This operation compares doubles + // with operator ==. Should it use a distance + // threshold instead? +} + +TEST_F(VectorTest, OperatorNotEqualIsntDefined) { + SUCCEED(); + //NL::Vector v3(4); + //EXPECT_TRUE(v1 != v3); // Not expressible in C++; + // gives compile time error +} + +TEST_F(VectorTest, VectorAssignment) { + NL::Vector v2(v1.size()); + v2 = v1; + EXPECT_EQ(v1, v2); +} + +#ifndef NDEBUG +TEST_F(VectorTest, AssignedVectorMustBeSameSize) { + NL::Vector v2(5); + // On Linux, the assertion message is: + // Assertion ... failed ... + // On OSX, it is: + // Assertion failed: (...), function ..., file ..., line ... + // Thus we just look for the word "Assertion". + EXPECT_DEATH({v2 = v1;}, "Assertion"); +} +#endif + +TEST_F(VectorTest, VectorScalesInplace) { + v1.scale(2); + EXPECT_EQ(v1.str(), "[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]"); +} + +TEST_F(VectorTest, VectorTranslatesInplace) { + v1.translate(1); + EXPECT_EQ(v1.str(), "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"); +} + +TEST_F(VectorTest, ScaleAndTranslateUsesFluentSyntax) { + NL::VectorView vv(v1, 3); + EXPECT_EQ(vv.translate(5).scale(10).str(), "[50, 60, 70]"); +} + +TEST_F(VectorTest, AddAssignment) { + NL::Vector v2(v1); + v2 += v1; + EXPECT_EQ(v2.str(), "[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]"); +} + +TEST_F(VectorTest, SubtractAssignment) { + NL::Vector v2(v1); + v2 -= v1; + EXPECT_EQ(v2.str(), "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]"); +} + +TEST_F(VectorTest, SwappingElements) { + v1.swap_elements(0, 9); + EXPECT_EQ(v1.str(), "[9, 1, 2, 3, 4, 5, 6, 7, 8, 0]"); +} + +TEST_F(VectorTest, Reverse) { + v1.reverse(); + EXPECT_EQ(v1.str(), "[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]"); +} + +TEST(Vector, IsPositive) { + EXPECT_TRUE(V3(1, 1, 1).is_positive()); + EXPECT_FALSE(V3(0, 0, 0).is_positive()); + EXPECT_FALSE(V3(-1, 0, 1).is_positive()); +} + +TEST_F(VectorTest, IsZero) { + EXPECT_FALSE(v1.is_zero()); + EXPECT_TRUE(V3(0, 0, 0).is_zero()); +} + +TEST_F(VectorTest, IsNonNegative) { + EXPECT_TRUE(V3(1, 1, 1).is_non_negative()); + EXPECT_TRUE(V3(0, 0, 0).is_non_negative()); + EXPECT_FALSE(V3(-1, 1, 1).is_non_negative()); +} + +TEST(Vector, Max) { + EXPECT_EQ(V3(1, 5, 3).max(), 5); +} + +TEST(Vector, MaxIndex) { + EXPECT_EQ(V3(1, 5, 3).max_index(), 1u); +} + +TEST(Vector, Min) { + EXPECT_EQ(V3(1, -5, -300).min(), -300); +} + +TEST(Vector, MinIndex) { + EXPECT_EQ(V3(1, 5, 3).min_index(), 0u); +} + +TEST_F(VectorTest, SetAll) { + v1.set_all(5); + EXPECT_EQ(v1.str(), "[5, 5, 5, 5, 5, 5, 5, 5, 5, 5]"); +} + +TEST_F(VectorTest, SetBasis) { + v1.set_basis(1); + EXPECT_EQ(v1.str(), "[0, 1, 0, 0, 0, 0, 0, 0, 0, 0]"); +} + +TEST(Vector, SwappingVectors) { + NL::Vector a(V3(1, 2, 3)); + NL::Vector b(V3(7, 7, 7)); + NL::swap(a, b); + EXPECT_EQ(V3(7, 7, 7), a); + EXPECT_EQ(V3(1, 2, 3), b); +} + +//// +// ** VectorView tests ** +//// + +// Construction examples + +TEST_F(VectorViewTest, ViewCountOnly) { + // VectorView(vector, showCount) + EXPECT_EQ(NL::VectorView(v1, 5).str(), "[0, 1, 2, 3, 4]"); +} + +TEST_F(VectorViewTest, SkipSomeInitialElements) { + // VectorView(vector, showCount, startIndex) + EXPECT_EQ(NL::VectorView(v1, 5, 3).str(), "[3, 4, 5, 6, 7]"); +} + +TEST_F(VectorViewTest, SparseViewConstruction) { + // VectorView(vector, showCount, startIndex, step) + EXPECT_EQ(NL::VectorView(v1, 5, 0, 2).str(), "[0, 2, 4, 6, 8]"); + EXPECT_EQ(NL::VectorView(v1, 5, 1, 2).str(), "[1, 3, 5, 7, 9]"); +} + +TEST_F(VectorViewTest, ConstructFromAnotherView) { + // VectorView(vectorview, showCount, startIndex, step) + NL::VectorView vv(v1, 5, 1, 2); + NL::VectorView view(vv, 3, 0, 2); + EXPECT_EQ(view.str(), "[1, 5, 9]"); +} + +// Operations modify source vectors + +TEST_F(VectorViewTest, PartialSourceModification) { + NL::VectorView vv(v1, 3); + vv.translate(10); + EXPECT_EQ(v1.str(), + "[10, 11, 12, 3, 4, 5, 6, 7, 8, 9]"); + EXPECT_EQ(vv.str(), + "[10, 11, 12]"); +} + +// Scale and translate examples + +TEST_F(VectorViewTest, ViewScalesInplace) { + v1.scale(10); + EXPECT_EQ(NL::VectorView(v1, 3).str(), "[0, 10, 20]"); +} + +TEST_F(VectorViewTest, ViewScaleAndTranslateUsesFluentSyntax) { + EXPECT_EQ(NL::VectorView(v1, 3).scale(10).translate(1).str(), + "[1, 11, 21]"); +} + +// Assignment + +TEST_F(VectorViewTest, AssignmentFromVectorAvailableForViews) { + NL::VectorView vv(v1, v1.size()); + vv = v1; + EXPECT_EQ(vv.str(), v1.str()); +} + +#ifndef NDEBUG +TEST_F(VectorViewTest, AssignmentFromVectorMustBeSameSize) { + NL::VectorView vv(v1, 5); + EXPECT_DEATH({vv = v1;}, "Assertion"); +} +#endif + +TEST_F(VectorViewTest, AssignmentFromViewAvailableForViews) { + NL::VectorView view1(v1, v1.size()); + view1 = v1; + NL::VectorView view2(view1); + view2 = view1; + EXPECT_EQ(view1.str(), view2.str()); +} + +#ifndef NDEBUG +TEST_F(VectorViewTest, AssignmentFromViewMustBeSameSize) { + NL::VectorView view1(v1, v1.size()); + NL::VectorView view2(view1, view1.size() - 1); + EXPECT_DEATH({view2 = view1;}, "Assertion"); +} +#endif + +// Add- and subtract assignment + +TEST_F(VectorViewTest, AddAssignAvailableForViews) { + NL::VectorView v2(v1); + v1 += v2; + EXPECT_EQ(v1.str(), "[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]"); +} + +TEST_F(VectorViewTest, SubtractAssignAvailableForViews) { + NL::Vector v2(v1); + v1 -= v2; + EXPECT_TRUE(v1.is_zero()); +} + +// View swapping + +TEST_F(VectorViewTest, SwappingFromSameSourceVectorDoesNotModifySource) { + NL::VectorView vv1(v1, 2, 0); + NL::VectorView vv2(v1, 2, 8); + NL::swap_view(vv1, vv2); + EXPECT_EQ(v1.str(), "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"); +} + +TEST_F(VectorViewTest, SwappingFromSameSourceVectorModifiesViews) { + NL::VectorView viewStart(v1, 2, 0); + NL::VectorView viewEnd(v1, 2, 8); + EXPECT_EQ(viewStart.str(), "[0, 1]"); + EXPECT_EQ(viewEnd.str(), "[8, 9]"); + NL::swap_view(viewStart, viewEnd); + EXPECT_EQ(viewStart.str(), "[8, 9]"); + EXPECT_EQ(viewEnd.str(), "[0, 1]"); +} + +#ifndef NDEBUG +TEST_F(VectorViewTest, SwappingDifferentLengthViewFails) { + NL::VectorView vv1(v1, 4); + NL::VectorView vv2(v1, 3); + EXPECT_DEATH({NL::swap_view(vv1, vv2);}, "Assertion"); +} +#endif + + +} // namespace Geom diff --git a/tests/parallelogram-test.cpp b/tests/parallelogram-test.cpp new file mode 100644 index 0000000..70ccea1 --- /dev/null +++ b/tests/parallelogram-test.cpp @@ -0,0 +1,161 @@ +/** @file + * @brief Unit tests for Parallelogram + * + * Includes all tests from RotatedRect to demonstrate that it is a generalized + * version of the rotated rectangle. + */ +/* + * Authors: + * Thomas Holder + * Sergei Izmailov + * + * SPDX-License-Identifier: LGPL-2.1 or MPL-1.1 + */ + +#include <2geom/coord.h> +#include <2geom/parallelogram.h> +#include <2geom/transforms.h> + +#include <gtest/gtest.h> + +using namespace Geom; + +// Analogous to RotatedRect::from_rect_rotate +static Parallelogram parallelogram_from_rect_rotate(Rect const &rect, Rotate const &rotate, Point const &point) +{ + Affine affine = Translate(-point) * rotate * Translate(point); + return Parallelogram(rect) * affine; +} +static Parallelogram parallelogram_from_rect_rotate(Rect const &rect, Rotate const &rotate) +{ + return parallelogram_from_rect_rotate(rect, rotate, rect.midpoint()); +} + +TEST(ParallelogramTest, midpoint) +{ + Rect r(-0.5, -0.5, 5.5, 5.5); + auto center = Point(2.5, 2.5); + + EXPECT_EQ(r.midpoint(), center); + for (double angle : { 0, 1, 25, 45, 90, 135 }) { + auto rotated_rect = parallelogram_from_rect_rotate(r, Rotate::from_degrees(angle), Point(0, 0)); + auto rotated_center = center * Rotate(angle / 180.0 * M_PI); + EXPECT_TRUE(Geom::are_near(rotated_rect.midpoint(), rotated_center, 1e-6)) << "Angle = " << angle << " deg"; + } +} + +TEST(ParallelogramTest, containsPoint1) +{ + Rect r(0, 0, 1, 1); + auto rotated_rect = r; + EXPECT_TRUE(rotated_rect.contains(Point(0, 0))); + EXPECT_TRUE(rotated_rect.contains(Point(1, 1))); + EXPECT_TRUE(rotated_rect.contains(Point(0.5, 0.5))); + EXPECT_FALSE(rotated_rect.contains(Point(1.1, 0.5))); + EXPECT_FALSE(rotated_rect.contains(Point(0.5, 1.1))); +} + +TEST(ParallelogramTest, containsPoint2) +{ + Rect r(0, 0, 1, 1); + auto rotated_rect = parallelogram_from_rect_rotate(r, Rotate::from_degrees(45), Point(0, 0)); + EXPECT_TRUE(rotated_rect.contains(Point(0, 0))); + EXPECT_TRUE(rotated_rect.contains(Point(0, 1.2))); + EXPECT_TRUE(rotated_rect.contains(Point(0.5, 0.9))); + EXPECT_FALSE(rotated_rect.contains(Point(1, 1))); + EXPECT_FALSE(rotated_rect.contains(Point(0.1, 0))); +} + +TEST(ParallelogramTest, intersects_aligned) +{ + Rect r(0, 0, 1, 1); + auto rotated_rect = r; + // point within rect + EXPECT_TRUE(rotated_rect.intersects(Rect(-1, -1, 2, 2))); + EXPECT_TRUE(rotated_rect.intersects(Rect(0.1, 0.1, 0.2, 0.2))); + EXPECT_TRUE(rotated_rect.intersects(Rect(-0.1, -0.1, 0.1, 0.1))); + EXPECT_FALSE(rotated_rect.intersects(Rect(-0.2, -0.2, -0.1, -0.1))); + EXPECT_FALSE(rotated_rect.intersects(Rect(1.1, 1.1, 1.2, 1.2))); + // edge intersection + EXPECT_TRUE(rotated_rect.intersects(Rect(0.5, -0.1, 0.6, 1.2))); + EXPECT_TRUE(rotated_rect.intersects(Rect(-0.1, 0.5, 1.2, 0.6))); +} + +TEST(ParallelogramTest, bounds) +{ + auto r = Rect::from_xywh(1.260, 0.547, 8.523, 11.932); + auto rrect = parallelogram_from_rect_rotate(r, Rotate::from_degrees(15.59)); + auto bbox = rrect.bounds(); + auto expected_bbox = Rect::from_xywh(-0.186, -0.378, 11.415, 13.783); + for (int i = 0; i < 4; i++) { + EXPECT_TRUE(Geom::are_near(bbox.corner(i), expected_bbox.corner(i), 1e-3)); + } +} + +TEST(ParallelogramTest, isSheared) +{ + Parallelogram p(Rect(2, 4, 7, 8)); + EXPECT_FALSE(p.isSheared()); + p *= Rotate(M_PI / 4.0); // 45° + EXPECT_FALSE(p.isSheared()); + p *= HShear(2); + EXPECT_TRUE(p.isSheared()); +} + +TEST(ParallelogramTest, area) +{ + Rect r(2, 4, 7, 8); + Parallelogram p(r); + EXPECT_DOUBLE_EQ(p.area(), r.area()); + p *= Rotate(M_PI / 4.0); // 45° + EXPECT_DOUBLE_EQ(p.area(), r.area()); + p *= HShear(2); + EXPECT_DOUBLE_EQ(p.area(), r.area()); + p *= Scale(2); + EXPECT_DOUBLE_EQ(p.area(), r.area() * 4); +} + +class ParallelogramTest + : public testing::TestWithParam<std::tuple<Rect /*rect*/, double /*degrees*/, bool /*intersects*/>> { + + void SetUp() override { target = Rect::from_xywh(0, 0, 11, 13); } + + public: + Rect target; +}; + +TEST_P(ParallelogramTest, intersects) +{ + Rect rect; + double degrees; + bool intersects; + std::tie(rect, degrees, intersects) = GetParam(); + EXPECT_EQ(parallelogram_from_rect_rotate(rect, Rotate::from_degrees(degrees)).intersects(target), intersects) + << "ERROR: rect {" << rect << "} rotated by {" << degrees << "} degrees " << (!intersects ? "" : "NOT ") + << "intersects with {" << target << "} but MUST " << (intersects ? "" : "NOT"); +} + +// clang-format off +INSTANTIATE_TEST_CASE_P(intesect_non_aligned, ParallelogramTest, + testing::Values( + std::make_tuple(Rect::from_xywh(10.456, -4.479, 7, 5), 0, true), + std::make_tuple(Rect::from_xywh(10.456, -4.479, 7, 5), 15, false), + std::make_tuple(Rect::from_xywh(9.929, 12.313, 7, 5), 93.2, false), + std::make_tuple(Rect::from_xywh(9.929, 12.313, 7, 5), 91.37, true), + std::make_tuple(Rect::from_xywh(-1, 4, 13, 3), 0, true), + std::make_tuple(Rect::from_xywh(4, -2, 3, 16), 0, true), + std::make_tuple(Rect::from_xywh(-5.113, -3.283, 5.000, 7.000), 11.81, false), + std::make_tuple(Rect::from_xywh(-5.113, -3.283, 5.000, 7.000), 13.35, true), + std::make_tuple(Rect::from_xywh(1.260, 0.547, 8.523, 11.932), 15.59, true), + std::make_tuple(Rect::from_xywh(5.328, 0.404, 11, 2), 28.16, true), + std::make_tuple(Rect::from_xywh(4.853, 10.691, 11, 2), -30.4, true), + std::make_tuple(Rect::from_xywh(-4.429, 10.752, 11, 2), 29.7, true), + std::make_tuple(Rect::from_xywh(-4.538, 0.314, 11, 2), -34.19, true), + std::make_tuple(Rect::from_xywh(8.398, -3.790, 2, 11), -34, true), + std::make_tuple(Rect::from_xywh(8.614, 6.163, 2, 11), 30.38, true), + std::make_tuple(Rect::from_xywh(0.492, 6.904, 2, 11), -37.29, true), + std::make_tuple(Rect::from_xywh(0.202, -3.148, 2, 11), 31.12, true))); + +// clang-format on + +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/tests/parser-test.py b/tests/parser-test.py new file mode 100644 index 0000000..86b2deb --- /dev/null +++ b/tests/parser-test.py @@ -0,0 +1,94 @@ +# * A simple toy to test the parser +# * +# * Copyright 2008 Aaron Spike <aaron@ekips.org> +# * +# * 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. + +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "py2geom")) +import py2geom + +class TestSink(py2geom.SVGPathSink): + def __init__(self): + py2geom.SVGPathSink.__init__(self) + self.data = [] + def __str__(self): + return ' '.join(self.data) + def moveTo(self, p): + x,y = p + self.data.append('M %s, %s' % (x,y)) + def lineTo(self, p): + x,y = p + self.data.append('L %s, %s' % (x,y)) + def curveTo(self, c0, c1, p): + c0x,c0y = c0 + c1x,c1y = c1 + x,y = p + self.data.append('C %s, %s %s, %s %s, %s' % (c0x,c0y,c1x,c1y,x,y)) + def quadTo(self, c, p): + cx,cy = c + x,y = p + self.data.append('Q %s, %s %s, %s' % (cx,cy,x,y)) + def arcTo(self, rx, ry, angle, large_arc, sweep, p): + x,y = p + self.data.append('A %s, %s %s %i %i %s, %s' % (rx,ry,angle,large_arc,sweep,x,y)) + def closePath(self): + self.data.append('Z') + def flush(self): + pass + +def test_path(description, in_path, out_path): + s = TestSink() + py2geom.parse_svg_path(in_path, s) + if str(s) == out_path: + print 'Success: %s' % description + return True + else: + print 'Error: %s' % description + print ' given "%s"' % in_path + print ' got "%s"' % str(s) + print ' expected "%s"' % out_path + return False + +def run_tests(tests): + successes = 0 + failures = 0 + for description, in_path, out_path in tests: + if test_path(description, in_path, out_path): + successes += 1 + else: + failures += 1 + print '=' * 20 + print 'Tests: %s' % (successes + failures) + print 'Good: %s' % successes + print 'Bad: %s' % failures + +if __name__=='__main__': + tests = [ + ('lineto', 'M 10,10 L 4,4', 'M 10.0, 10.0 L 4.0, 4.0'), + ('implicit lineto', 'M 10,10 L 4,4 5,5 6,6', 'M 10.0, 10.0 L 4.0, 4.0 L 5.0, 5.0 L 6.0, 6.0'), + ('implicit lineto after moveto', 'M1.2.3.4.5.6.7', 'M 1.2, 0.3 L 0.4, 0.5 L 0.6, 0.7'), + ('arcto', 'M 300 150 A 150, 120, 30, 1, 0, 200 100', 'M 300.0, 150.0 A 150.0, 120.0 30.0 1 0 200.0, 100.0'), + ] + run_tests(tests) diff --git a/tests/path-test.cpp b/tests/path-test.cpp new file mode 100644 index 0000000..dd6f347 --- /dev/null +++ b/tests/path-test.cpp @@ -0,0 +1,991 @@ +#include <cmath> +#include <vector> +#include <iterator> +#include <iostream> + +#include <glib.h> + +#include <2geom/bezier.h> +#include <2geom/path.h> +#include <2geom/pathvector.h> +#include <2geom/path-intersection.h> +#include <2geom/svg-path-parser.h> +#include <2geom/svg-path-writer.h> + +#include "testing.h" + +using namespace std; +using namespace Geom; + +Path string_to_path(const char* s) { + PathVector pv = parse_svg_path(s); + assert(pv.size() == 1); + return pv[0]; +} + +// Path fixture +class PathTest : public ::testing::Test { +protected: + PathTest() { + line.append(LineSegment(Point(0,0), Point(1,0))); + square = string_to_path("M 0,0 1,0 1,1 0,1 z"); + circle = string_to_path("M 0,0 a 4.5,4.5 0 1 1 -9,0 4.5,4.5 0 1 1 9,0 z"); + arcs = string_to_path("M 0,0 a 5,10 45 0 1 10,10 a 5,10 45 0 1 0,0 z"); + diederik = string_to_path("m 262.6037,35.824151 c 0,0 -92.64892,-187.405851 30,-149.999981 104.06976,31.739531 170,109.9999815 170,109.9999815 l -10,-59.9999905 c 0,0 40,79.99999 -40,79.99999 -80,0 -70,-129.999981 -70,-129.999981 l 50,0 C 435.13571,-131.5667 652.76275,126.44872 505.74322,108.05672 358.73876,89.666591 292.6037,-14.175849 292.6037,15.824151 c 0,30 -30,20 -30,20 z"); + cmds = string_to_path("M 0,0 V 100 H 100 Q 100,0 0,0 L 200,0 C 200,100 300,100 300,0 S 200,-100 200,0"); + + p_open = string_to_path("M 0,0 L 0,5 5,5 5,0"); + p_closed = p_open; + p_closed.close(true); + p_add = string_to_path("M -1,6 L 6,6"); + + p_open.setStitching(true); + p_closed.setStitching(true); + } + + // Objects declared here can be used by all tests in the test case for Foo. + Path line, square, circle, arcs, diederik, cmds; + Path p_open, p_closed, p_add; +}; + +TEST_F(PathTest, CopyConstruction) { + Path pa = p_closed; + Path pc(p_closed); + EXPECT_EQ(pa, p_closed); + EXPECT_EQ(pa.closed(), p_closed.closed()); + EXPECT_EQ(pc, p_closed); + EXPECT_EQ(pc.closed(), p_closed.closed()); + + Path poa = cmds; + Path poc(cmds); + EXPECT_EQ(poa, cmds); + EXPECT_EQ(poa.closed(), cmds.closed()); + EXPECT_EQ(poc, cmds); + EXPECT_EQ(poc.closed(), cmds.closed()); + + PathVector pvc(pa); + EXPECT_EQ(pvc[0], pa); + PathVector pva((Geom::Path())); + pva[0] = pa; + EXPECT_EQ(pva[0], pa); +} + +TEST_F(PathTest, PathInterval) { + PathTime n2_before(1, 0.9995), n2_after(2, 0.0005), + n3_before(2, 0.9995), n3_after(3, 0.0005), + mid2(2, 0.5), mid3(3, 0.5); + + // ival[x][0] - normal + // ival[x][1] - reversed + // ival[x][2] - crosses start + // ival[x][3] - reversed, crosses start + PathInterval ival[5][4]; + + ival[0][0] = PathInterval(n2_before, n2_after, false, 4); + ival[0][1] = PathInterval(n2_after, n2_before, false, 4); + ival[0][2] = PathInterval(n2_before, n2_after, true, 4); + ival[0][3] = PathInterval(n2_after, n2_before, true, 4); + ival[1][0] = PathInterval(n2_before, n3_after, false, 4); + ival[1][1] = PathInterval(n3_after, n2_before, false, 4); + ival[1][2] = PathInterval(n2_before, n3_after, true, 4); + ival[1][3] = PathInterval(n3_after, n2_before, true, 4); + ival[2][0] = PathInterval(n2_before, mid2, false, 4); + ival[2][1] = PathInterval(mid2, n2_before, false, 4); + ival[2][2] = PathInterval(n2_before, mid2, true, 4); + ival[2][3] = PathInterval(mid2, n2_before, true, 4); + ival[3][0] = PathInterval(mid2, mid3, false, 4); + ival[3][1] = PathInterval(mid3, mid2, false, 4); + ival[3][2] = PathInterval(mid2, mid3, true, 4); + ival[3][3] = PathInterval(mid3, mid2, true, 4); + ival[4][0] = PathInterval(n2_after, n3_before, false, 4); + ival[4][1] = PathInterval(n3_before, n2_after, false, 4); + ival[4][2] = PathInterval(n2_after, n3_before, true, 4); + ival[4][3] = PathInterval(n3_before, n2_after, true, 4); + + EXPECT_TRUE(ival[0][0].contains(n2_before)); + EXPECT_TRUE(ival[0][0].contains(n2_after)); + EXPECT_TRUE(ival[0][1].contains(n2_before)); + EXPECT_TRUE(ival[0][1].contains(n2_after)); + + for (unsigned i = 0; i <= 4; ++i) { + EXPECT_FALSE(ival[i][0].reverse()); + EXPECT_TRUE(ival[i][1].reverse()); + EXPECT_TRUE(ival[i][2].reverse()); + EXPECT_FALSE(ival[i][3].reverse()); + } + + for (unsigned i = 0; i <= 4; ++i) { + for (unsigned j = 0; j <= 3; ++j) { + //std::cout << i << " " << j << " " << ival[i][j] << std::endl; + EXPECT_TRUE(ival[i][j].contains(ival[i][j].inside(1e-3))); + } + } + + PathTime n1(1, 0.0), n1x(0, 1.0), + n2(2, 0.0), n2x(1, 1.0), + n3(3, 0.0), n3x(2, 1.0); + PathTime tests[8] = { n1, n1x, n2, n2x, n3, n3x, mid2, mid3 }; + + // 0: false for both + // 1: true for normal, false for cross_start + // 2: false for normal, true for cross_start + // 3: true for both + + int const NORMAL = 1, CROSS = 2, BOTH = 3; + + int includes[5][8] = { + { CROSS, CROSS, NORMAL, NORMAL, CROSS, CROSS, CROSS, CROSS }, + { CROSS, CROSS, NORMAL, NORMAL, NORMAL, NORMAL, NORMAL, CROSS }, + { CROSS, CROSS, NORMAL, NORMAL, CROSS, CROSS, BOTH, CROSS }, + { CROSS, CROSS, CROSS, CROSS, NORMAL, NORMAL, BOTH, BOTH }, + { CROSS, CROSS, CROSS, CROSS, CROSS, CROSS, NORMAL, CROSS } + }; + unsigned sizes[5][2] = { + { 2, 4 }, + { 3, 3 }, + { 2, 4 }, + { 2, 4 }, + { 1, 5 } + }; + + for (unsigned i = 0; i < 5; ++i) { + for (unsigned j = 0; j < 8; ++j) { + EXPECT_EQ(ival[i][0].contains(tests[j]), bool(includes[i][j] & NORMAL)); + EXPECT_EQ(ival[i][1].contains(tests[j]), bool(includes[i][j] & NORMAL)); + EXPECT_EQ(ival[i][2].contains(tests[j]), bool(includes[i][j] & CROSS)); + EXPECT_EQ(ival[i][3].contains(tests[j]), bool(includes[i][j] & CROSS)); + } + EXPECT_EQ(ival[i][0].curveCount(), sizes[i][0]); + EXPECT_EQ(ival[i][1].curveCount(), sizes[i][0]); + EXPECT_EQ(ival[i][2].curveCount(), sizes[i][1]); + EXPECT_EQ(ival[i][3].curveCount(), sizes[i][1]); + } +} + +TEST_F(PathTest, Continuity) { + line.checkContinuity(); + square.checkContinuity(); + circle.checkContinuity(); + diederik.checkContinuity(); + cmds.checkContinuity(); +} + +TEST_F(PathTest, RectConstructor) { + Rect r(Point(0,0), Point(10,10)); + Path rpath(r); + + EXPECT_EQ(rpath.size(), 4u); + EXPECT_TRUE(rpath.closed()); + for (unsigned i = 0; i < 4; ++i) { + EXPECT_TRUE(dynamic_cast<LineSegment const *>(&rpath[i]) != NULL); + EXPECT_EQ(rpath[i].initialPoint(), r.corner(i)); + } +} + +TEST_F(PathTest, Reversed) { + std::vector<Path> a, r; + a.push_back(p_open); + a.push_back(p_closed); + a.push_back(circle); + a.push_back(diederik); + a.push_back(cmds); + + for (auto & i : a) { + r.push_back(i.reversed()); + } + + for (unsigned i = 0; i < a.size(); ++i) { + EXPECT_EQ(r[i].size(), a[i].size()); + EXPECT_EQ(r[i].initialPoint(), a[i].finalPoint()); + EXPECT_EQ(r[i].finalPoint(), a[i].initialPoint()); + EXPECT_EQ(r[i].reversed(), a[i]); + Point p1 = r[i].pointAt(0.75); + Point p2 = a[i].pointAt(a[i].size() - 0.75); + EXPECT_FLOAT_EQ(p1[X], p2[X]); + EXPECT_FLOAT_EQ(p1[Y], p2[Y]); + EXPECT_EQ(r[i].closed(), a[i].closed()); + a[i].checkContinuity(); + } +} + +TEST_F(PathTest, ValueAt) { + EXPECT_EQ(Point(0,0), line.initialPoint()); + EXPECT_EQ(Point(1,0), line.finalPoint()); + + EXPECT_EQ(Point(0.5, 0.0), line.pointAt(0.5)); + + EXPECT_EQ(Point(0,0), square.initialPoint()); + EXPECT_EQ(Point(0,0), square.finalPoint()); + EXPECT_EQ(Point(1,0), square.pointAt(1)); + EXPECT_EQ(Point(0.5,1), square.pointAt(2.5)); + EXPECT_EQ(Point(0,0.5), square.pointAt(3.5)); + EXPECT_EQ(Point(0,0), square.pointAt(4)); +} + +TEST_F(PathTest, NearestPoint) { + EXPECT_EQ(0, line.nearestTime(Point(0,0)).asFlatTime()); + EXPECT_EQ(0.5, line.nearestTime(Point(0.5,0)).asFlatTime()); + EXPECT_EQ(0.5, line.nearestTime(Point(0.5,1)).asFlatTime()); + EXPECT_EQ(1, line.nearestTime(Point(100,0)).asFlatTime()); + EXPECT_EQ(0, line.nearestTime(Point(-100,1000)).asFlatTime()); + + EXPECT_EQ(0, square.nearestTime(Point(0,0)).asFlatTime()); + EXPECT_EQ(1, square.nearestTime(Point(1,0)).asFlatTime()); + EXPECT_EQ(3, square.nearestTime(Point(0,1)).asFlatTime()); + + //cout << diederik.nearestTime(Point(247.32293,-43.339507)) << endl; + + Point p(511.75,40.85); + EXPECT_FLOAT_EQ(6.5814033, diederik.nearestTime(p).asFlatTime()); + /*cout << diederik.pointAt(diederik.nearestTime(p)) << endl + << diederik.pointAt(6.5814033) << endl + << distance(diederik.pointAt(diederik.nearestTime(p)), p) << " " + << distance(diederik.pointAt(6.5814033), p) << endl;*/ + +} + +TEST_F(PathTest, Winding) { + // test points in special positions + EXPECT_EQ(line.winding(Point(-1, 0)), 0); + EXPECT_EQ(line.winding(Point(2, 0)), 0); + EXPECT_EQ(line.winding(Point(0, 1)), 0); + EXPECT_EQ(line.winding(Point(0, -1)), 0); + EXPECT_EQ(line.winding(Point(1, 1)), 0); + EXPECT_EQ(line.winding(Point(1, -1)), 0); + + EXPECT_EQ(square.winding(Point(0, -1)), 0); + EXPECT_EQ(square.winding(Point(1, -1)), 0); + EXPECT_EQ(square.winding(Point(0, 2)), 0); + EXPECT_EQ(square.winding(Point(1, 2)), 0); + EXPECT_EQ(square.winding(Point(-1, 0)), 0); + EXPECT_EQ(square.winding(Point(-1, 1)), 0); + EXPECT_EQ(square.winding(Point(2, 0)), 0); + EXPECT_EQ(square.winding(Point(2, 1)), 0); + EXPECT_EQ(square.winding(Point(0.5, 0.5)), 1); + + EXPECT_EQ(circle.winding(Point(-4.5,0)), 1); + EXPECT_EQ(circle.winding(Point(-3.5,0)), 1); + EXPECT_EQ(circle.winding(Point(-4.5,1)), 1); + EXPECT_EQ(circle.winding(Point(-10,0)), 0); + EXPECT_EQ(circle.winding(Point(1,0)), 0); + + Path yellipse = string_to_path("M 0,0 A 40 20 90 0 0 0,-80 40 20 90 0 0 0,0 z"); + EXPECT_EQ(yellipse.winding(Point(-1, 0)), 0); + EXPECT_EQ(yellipse.winding(Point(-1, -80)), 0); + EXPECT_EQ(yellipse.winding(Point(1, 0)), 0); + EXPECT_EQ(yellipse.winding(Point(1, -80)), 0); + EXPECT_EQ(yellipse.winding(Point(0, -40)), -1); + std::vector<double> r[4]; + r[0] = yellipse[0].roots(0, Y); + r[1] = yellipse[0].roots(-80, Y); + r[2] = yellipse[1].roots(0, Y); + r[3] = yellipse[1].roots(-80, Y); + for (auto & i : r) { + for (double j : i) { + std::cout << format_coord_nice(j) << " "; + } + std::cout << std::endl; + } + std::cout << yellipse[0].unitTangentAt(0) << " " + << yellipse[0].unitTangentAt(1) << " " + << yellipse[1].unitTangentAt(0) << " " + << yellipse[1].unitTangentAt(1) << std::endl; + + Path half_ellipse = string_to_path("M 0,0 A 40 20 90 0 0 0,-80 L -20,-40 z"); + EXPECT_EQ(half_ellipse.winding(Point(-1, 0)), 0); + EXPECT_EQ(half_ellipse.winding(Point(-1, -80)), 0); + EXPECT_EQ(half_ellipse.winding(Point(1, 0)), 0); + EXPECT_EQ(half_ellipse.winding(Point(1, -80)), 0); + EXPECT_EQ(half_ellipse.winding(Point(0, -40)), -1); + + // extra nasty cases with exact double roots + Path hump = string_to_path("M 0,0 Q 1,1 2,0 L 2,2 0,2 Z"); + EXPECT_EQ(hump.winding(Point(0.25, 0.5)), 1); + EXPECT_EQ(hump.winding(Point(1.75, 0.5)), 1); + + Path hump2 = string_to_path("M 0,0 L 2,0 2,2 Q 1,1 0,2 Z"); + EXPECT_EQ(hump2.winding(Point(0.25, 1.5)), 1); + EXPECT_EQ(hump2.winding(Point(1.75, 1.5)), 1); +} + +/// Regression test for issue https://gitlab.com/inkscape/lib2geom/-/issues/58 +TEST_F(PathTest, Issue58) +{ + auto const random_point_in = [](Geom::Rect const &box) -> Point { + Coord const x = g_random_double_range(box[X].min(), box[X].max()); + Coord const y = g_random_double_range(box[Y].min(), box[Y].max()); + return {x, y}; + }; + + auto const verify_windings = [](Ellipse const &e, Path const &path, Point const &pt) { + int const winding = path.winding(pt); + if (e.contains(pt)) { + EXPECT_EQ(winding, 1); + } else { + EXPECT_EQ(winding, 0); + } + }; + + // Example elliptical path from issue https://gitlab.com/inkscape/lib2geom/-/issues/58 + char const *const issue_d = "M 495.8157837290847 280.07459226562503" + "A 166.63407933993605 132.04407218873035 0 0 1 329.1817043891487 412.11866445435544" + "A 166.63407933993605 132.04407218873035 0 0 1 162.54762504921263 280.07459226562503" + "A 166.63407933993605 132.04407218873035 0 0 1 329.1817043891487 148.0305200768947" + "A 166.63407933993605 132.04407218873035 0 0 1 495.8157837290847 280.07459226562503" + "z"; + auto const pv = parse_svg_path(issue_d); + auto const issue_ellipse = Ellipse(Point(329.1817043891487, 280.07459226562503), + Point(166.63407933993605, 132.04407218873035), 0); + + auto box = issue_ellipse.boundsExact(); + box.expandBy(1.0); + + g_random_set_seed(0xE111BB5E); + for (size_t _ = 0; _ < 10'000; _++) { + verify_windings(issue_ellipse, pv[0], random_point_in(box)); + } +} + +TEST_F(PathTest, SVGRoundtrip) { + SVGPathWriter sw; + + Path transformed = diederik * (Rotate(1.23456789) * Scale(1e-8) * Translate(1e-9, 1e-9)); + + for (unsigned i = 0; i < 4; ++i) { + sw.setOptimize(i & 1); + sw.setUseShorthands(i & 2); + + sw.feed(line); + //cout << sw.str() << endl; + Path line_svg = string_to_path(sw.str().c_str()); + EXPECT_TRUE(line_svg == line); + sw.clear(); + + sw.feed(square); + //cout << sw.str() << endl; + Path square_svg = string_to_path(sw.str().c_str()); + EXPECT_TRUE(square_svg == square); + sw.clear(); + + sw.feed(circle); + //cout << sw.str() << endl; + Path circle_svg = string_to_path(sw.str().c_str()); + EXPECT_TRUE(circle_svg == circle); + sw.clear(); + + sw.feed(arcs); + //cout << sw.str() << endl; + Path arcs_svg = string_to_path(sw.str().c_str()); + EXPECT_TRUE(arcs_svg == arcs); + sw.clear(); + + sw.feed(diederik); + //cout << sw.str() << endl; + Path diederik_svg = string_to_path(sw.str().c_str()); + EXPECT_TRUE(diederik_svg == diederik); + sw.clear(); + + sw.feed(transformed); + //cout << sw.str() << endl; + Path transformed_svg = string_to_path(sw.str().c_str()); + EXPECT_TRUE(transformed_svg == transformed); + sw.clear(); + + sw.feed(cmds); + //cout << sw.str() << endl; + Path cmds_svg = string_to_path(sw.str().c_str()); + EXPECT_TRUE(cmds_svg == cmds); + sw.clear(); + } +} + +TEST_F(PathTest, Portion) { + PathTime a(0, 0.5), b(3, 0.5); + PathTime c(1, 0.25), d(1, 0.75); + + EXPECT_EQ(square.portion(a, b), string_to_path("M 0.5, 0 L 1,0 1,1 0,1 0,0.5")); + EXPECT_EQ(square.portion(b, a), string_to_path("M 0,0.5 L 0,1 1,1 1,0 0.5,0")); + EXPECT_EQ(square.portion(a, b, true), string_to_path("M 0.5,0 L 0,0 0,0.5")); + EXPECT_EQ(square.portion(b, a, true), string_to_path("M 0,0.5 L 0,0 0.5,0")); + EXPECT_EQ(square.portion(c, d), string_to_path("M 1,0.25 L 1,0.75")); + EXPECT_EQ(square.portion(d, c), string_to_path("M 1,0.75 L 1,0.25")); + EXPECT_EQ(square.portion(c, d, true), string_to_path("M 1,0.25 L 1,0 0,0 0,1 1,1 1,0.75")); + EXPECT_EQ(square.portion(d, c, true), string_to_path("M 1,0.75 L 1,1 0,1 0,0 1,0 1,0.25")); + + // verify that no matter how an endpoint is specified, the result is the same + PathTime a1(0, 1.0), a2(1, 0.0); + PathTime b1(2, 1.0), b2(3, 0.0); + Path result = string_to_path("M 1,0 L 1,1 0,1"); + EXPECT_EQ(square.portion(a1, b1), result); + EXPECT_EQ(square.portion(a1, b2), result); + EXPECT_EQ(square.portion(a2, b1), result); + EXPECT_EQ(square.portion(a2, b2), result); +} + +TEST_F(PathTest, AppendSegment) { + Path p_open = line, p_closed = line; + p_open.setStitching(true); + p_open.append(new LineSegment(Point(10,20), Point(10,25))); + EXPECT_EQ(p_open.size(), 3u); + EXPECT_NO_THROW(p_open.checkContinuity()); + + p_closed.setStitching(true); + p_closed.close(true); + p_closed.append(new LineSegment(Point(10,20), Point(10,25))); + EXPECT_EQ(p_closed.size(), 4u); + EXPECT_NO_THROW(p_closed.checkContinuity()); +} + +TEST_F(PathTest, AppendPath) { + p_open.append(p_add); + Path p_expected = string_to_path("M 0,0 L 0,5 5,5 5,0 -1,6 6,6"); + EXPECT_EQ(p_open.size(), 5u); + EXPECT_EQ(p_open, p_expected); + EXPECT_NO_THROW(p_open.checkContinuity()); + + p_expected.close(true); + p_closed.append(p_add); + EXPECT_EQ(p_closed.size(), 6u); + EXPECT_EQ(p_closed, p_expected); + EXPECT_NO_THROW(p_closed.checkContinuity()); +} + +TEST_F(PathTest, AppendPortion) { + // A closed path with two curves: + Path bigon = string_to_path("M 0,0 Q 1,1 2,0 Q 1,-1 0,0 Z"); + Path target{Point(0, 0)}; + + PathTime end_time{1, 1.0}; // End of the closed path + PathTime mid_time{1, 0.0}; // Middle of the closed path (juncture between the two curves) + bigon.appendPortionTo(target, end_time, mid_time, true /* do cross start */); + + // We expect that the target path now contains the entire first curve "M 0,0 Q 1,1 2,0", + // since we started at the end of a closed path and requested to cross its start. + EXPECT_EQ(target.size(), 1); + EXPECT_EQ(target, string_to_path("M 0,0 Q 1,1 2,0")); + + // Similar test but with reversal (swapped times) + Path target_reverse{Point(2, 0)}; + bigon.appendPortionTo(target_reverse, mid_time, end_time, true /* do cross start please */); + // What do we expect? To cross start going from the midpoint to the endpoint requires + // not taking the obvious route (bigon[1]) but rather taking bigon[0] in reverse. + EXPECT_EQ(target_reverse.size(), 1); + EXPECT_EQ(target_reverse, string_to_path("M 2,0 Q 1,1 0,0")); + + // Similar test but using start time + PathTime start_time{0, 0.0}; + Path mid_target{Point(2, 0)}; + bigon.appendPortionTo(mid_target, mid_time, start_time, true /* cross start to 0:0 */); + // We expect to go forward from mid_time and cross over the start to start_time. + EXPECT_EQ(mid_target.size(), 1); + EXPECT_EQ(mid_target, string_to_path("M 2,0 Q 1,-1 0,0")); + + // Use start time with reversal + Path mid_reverse{Point(0, 0)}; + bigon.appendPortionTo(mid_reverse, start_time, mid_time, true /* Cross start, going backwards. */); + // We expect that we don't go forwards from start_time to mid_time, but rather cross over the starting + // point and backtrack over bigon[1] to the midpoint. + EXPECT_EQ(mid_reverse.size(), 1); + EXPECT_EQ(mid_reverse, string_to_path("M 0,0 Q 1,-1 2,0")); +} + +TEST_F(PathTest, ReplaceMiddle) { + p_open.replace(p_open.begin() + 1, p_open.begin() + 2, p_add); + EXPECT_EQ(p_open.size(), 5u); + EXPECT_NO_THROW(p_open.checkContinuity()); + + p_closed.replace(p_closed.begin() + 1, p_closed.begin() + 2, p_add); + EXPECT_EQ(p_closed.size(), 6u); + EXPECT_NO_THROW(p_closed.checkContinuity()); +} + +TEST_F(PathTest, ReplaceStart) { + p_open.replace(p_open.begin(), p_open.begin() + 2, p_add); + EXPECT_EQ(p_open.size(), 3u); + EXPECT_NO_THROW(p_open.checkContinuity()); + + p_closed.replace(p_closed.begin(), p_closed.begin() + 2, p_add); + EXPECT_EQ(p_closed.size(), 5u); + EXPECT_NO_THROW(p_closed.checkContinuity()); +} + +TEST_F(PathTest, ReplaceEnd) { + p_open.replace(p_open.begin() + 1, p_open.begin() + 3, p_add); + EXPECT_EQ(p_open.size(), 3u); + EXPECT_NO_THROW(p_open.checkContinuity()); + + p_closed.replace(p_closed.begin() + 1, p_closed.begin() + 3, p_add); + EXPECT_EQ(p_closed.size(), 5u); + EXPECT_NO_THROW(p_closed.checkContinuity()); +} + +TEST_F(PathTest, ReplaceClosing) { + p_open.replace(p_open.begin() + 1, p_open.begin() + 4, p_add); + EXPECT_EQ(p_open.size(), 3u); + EXPECT_NO_THROW(p_open.checkContinuity()); + + p_closed.replace(p_closed.begin() + 1, p_closed.begin() + 4, p_add); + EXPECT_EQ(p_closed.size(), 4u); + EXPECT_NO_THROW(p_closed.checkContinuity()); +} + +TEST_F(PathTest, ReplaceEverything) { + p_open.replace(p_open.begin(), p_open.end(), p_add); + EXPECT_EQ(p_open.size(), 1u); + EXPECT_NO_THROW(p_open.checkContinuity()); + + // TODO: in this specific case, it may make sense to set the path to open... + // Need to investigate what behavior is sensible here + p_closed.replace(p_closed.begin(), p_closed.end(), p_add); + EXPECT_EQ(p_closed.size(), 2u); + EXPECT_NO_THROW(p_closed.checkContinuity()); +} + +TEST_F(PathTest, EraseLast) { + p_open.erase_last(); + Path p_expected = string_to_path("M 0,0 L 0,5 5,5"); + EXPECT_EQ(p_open, p_expected); + EXPECT_NO_THROW(p_open.checkContinuity()); +} + +TEST_F(PathTest, AreNear) { + Path nudged_arcs1 = string_to_path("M 0,0 a 5,10 45 0 1 10,10.0000005 a 5,10 45 0 1 0,0 z"); + Path nudged_arcs2 = string_to_path("M 0,0 a 5,10 45 0 1 10,10.00005 a 5,10 45 0 1 0,0 z"); + EXPECT_EQ(are_near(diederik, diederik, 0), true); + EXPECT_EQ(are_near(cmds, diederik, 1e-6), false); + EXPECT_EQ(are_near(arcs, nudged_arcs1, 1e-6), true); + EXPECT_EQ(are_near(arcs, nudged_arcs2, 1e-6), false); +} + +TEST_F(PathTest, Roots) { + Path path; + path.start(Point(0, 0)); + path.appendNew<Geom::LineSegment>(Point(1, 1)); + path.appendNew<Geom::LineSegment>(Point(2, 0)); + + EXPECT_FALSE(path.closed()); + + // Trivial case: make sure that path is not closed + std::vector<PathTime> roots = path.roots(0.5, Geom::X); + EXPECT_EQ(roots.size(), 1u); + EXPECT_EQ(path.valueAt(roots[0], Geom::Y), 0.5); + + // Now check that it is closed if we make it so + path.close(true); + roots = path.roots(0.5, Geom::X); + EXPECT_EQ(roots.size(), 2u); +} + +TEST_F(PathTest, PartingPoint) +{ + // === Test complete overlaps between identical curves === + // Line segment + auto line = string_to_path("M 0,0 L 3.33, 7.77"); + auto pt = parting_point(line, line); + EXPECT_TRUE(are_near(pt.point(), line.finalPoint())); + EXPECT_TRUE(are_near(pt.first.t, 1.0)); + + // Cubic Bézier + auto bezier = string_to_path("M 0,0 C 1,1 14,1 15,0"); + pt = parting_point(bezier, bezier); + EXPECT_TRUE(are_near(pt.point(), bezier.finalPoint())); + EXPECT_TRUE(are_near(pt.first.t, 1.0)); + + // Eliptical arc + auto const arc = string_to_path("M 0,0 A 100,20 0,0,0 200,0"); + pt = parting_point(arc, arc); + EXPECT_TRUE(are_near(pt.point(), arc.finalPoint())); + EXPECT_TRUE(are_near(pt.first.t, 1.0)); + + // === Test complete overlap between degree-elevated and degree-shrunk Béziers === + auto artificially_cubic = string_to_path("M 0,0 C 10,10 20,10 30,0"); + auto really_quadratic = string_to_path("M 0,0 Q 15,15 30,0"); + pt = parting_point(artificially_cubic, really_quadratic); + EXPECT_TRUE(are_near(pt.point(), artificially_cubic.finalPoint())); + EXPECT_TRUE(are_near(pt.first.asFlatTime(), 1.0)); + EXPECT_TRUE(are_near(pt.second.asFlatTime(), 1.0)); + + // === Test complete overlaps between a curve and its subdivision === + // Straight line + line = string_to_path("M 0,0 L 15,15"); + auto subdivided_line = string_to_path("M 0,0 L 3,3 L 4,4 L 9,9 L 15,15"); + pt = parting_point(line, subdivided_line); + EXPECT_TRUE(are_near(pt.point(), line.finalPoint())); + EXPECT_TRUE(are_near(pt.first.t, 1.0)); + + // Cubic Bézier + bezier = string_to_path("M 0,0 C 0,40 50,40 50,0"); + auto de_casteljau = string_to_path("M 0,0 C 0,10 3.125,17.5 7.8125,22.5 12.5,27.5 18.75,30 25,30" + " 31.25,30 37.5,27.5 42.1875,22.5 46.875,17.5 50,10 50,0"); + pt = parting_point(bezier, de_casteljau); + EXPECT_TRUE(are_near(pt.point(), bezier.finalPoint())); + EXPECT_TRUE(are_near(pt.first.t, 1.0)); + + // Eliptical arc + auto subdivided_arc = string_to_path("M 0,0 A 100,20, 0,0,0 100,20 A 100,20 0,0,0 200,0"); + pt = parting_point(arc, subdivided_arc); + EXPECT_TRUE(are_near(pt.point(), arc.finalPoint())); + EXPECT_TRUE(are_near(pt.first.t, 1.0)); + + // === Test complete overlap between different subdivisions === + auto line1 = string_to_path("M 0,0 L 3,3 L 5,5 L 10,10"); + auto line2 = string_to_path("M 0,0 L 2,2 L 4.2,4.2 L 4.5,4.5 L 6,6 L 10,10"); + pt = parting_point(line1, line2); + EXPECT_TRUE(are_near(pt.point(), line1.finalPoint())); + EXPECT_TRUE(are_near(pt.first.asFlatTime(), line1.timeRange().max())); + EXPECT_TRUE(are_near(pt.second.asFlatTime(), line2.timeRange().max())); + + // === Test complete overlaps in the presence of degenerate segments === + // Straight line + line = string_to_path("M 0,0 L 15,15"); + subdivided_line = string_to_path("M 0,0 L 3,3 H 3 V 3 L 3,3 L 4,4 H 4 V 4 L 4,4 L 9,9 H 9 L 15,15"); + pt = parting_point(line, subdivided_line); + EXPECT_TRUE(are_near(pt.point(), line.finalPoint())); + EXPECT_TRUE(are_near(pt.first.asFlatTime(), 1.0)); + + // Eliptical arc + auto arc_degen = string_to_path("M 0,0 A 100,20, 0,0,0 100,20 H 100 V 20 L 100,20 A 100,20 0,0,0 200,0"); + pt = parting_point(arc, arc_degen); + EXPECT_TRUE(are_near(pt.point(), arc.finalPoint())); + EXPECT_TRUE(are_near(pt.first.asFlatTime(), 1.0)); + + // === Paths that overlap but one is shorter than the other === + // Straight lines + auto long_line = string_to_path("M 0,0 L 20,10"); + auto short_line = string_to_path("M 0,0 L 4,2"); + pt = parting_point(long_line, short_line); + EXPECT_TRUE(are_near(pt.point(), short_line.finalPoint())); + EXPECT_TRUE(are_near(pt.first.t, 0.2)); + EXPECT_TRUE(are_near(pt.second.t, 1.0)); + + // Cubic Bézier + auto const s_shape = string_to_path("M 0,0 C 10, 0 0,10 10,10"); + auto half_s = string_to_path("M 0,0 C 5,0 5,2.5 5,5"); + pt = parting_point(s_shape, half_s); + EXPECT_TRUE(are_near(pt.first.t, 0.5)); + EXPECT_TRUE(are_near(pt.second.t, 1.0)); + + // Elliptical arc + auto quarter_ellipse = string_to_path("M 0,0 A 100,20, 0,0,0 100,20"); + pt = parting_point(arc, quarter_ellipse); + EXPECT_TRUE(are_near(pt.point(), quarter_ellipse.finalPoint())); + EXPECT_TRUE(are_near(pt.first.t, 0.5)); + EXPECT_TRUE(are_near(pt.second.t, 1.0)); + + // === Paths that overlap initially but then they split === + // Straight lines + auto boring_line = string_to_path("M 0,0 L 50,10"); + auto line_then_arc = string_to_path("M 0,0 L 5,1 A 1,1 0,0,0 7,1"); + pt = parting_point(boring_line, line_then_arc); + EXPECT_TRUE(are_near(pt.point(), Point(5, 1))); + EXPECT_TRUE(are_near(pt.first.t, 0.1)); + EXPECT_TRUE(are_near(pt.second.asFlatTime(), 1.0)); + + // Cubic Bézier + auto half_s_then_line = string_to_path("M 0,0 C 5,0 5,2.5 5,5 L 10,10"); + pt = parting_point(s_shape, half_s_then_line); + EXPECT_TRUE(are_near(pt.point(), Point(5, 5))); + EXPECT_TRUE(are_near(pt.first.t, 0.5)); + EXPECT_TRUE(are_near(pt.second.asFlatTime(), 1.0)); + + // Elliptical arc + auto quarter_ellipse_then_quadratic = string_to_path("M 0,0 A 100,20, 0,0,0 100,20 Q 120,40 140,60"); + pt = parting_point(arc, quarter_ellipse_then_quadratic); + EXPECT_TRUE(are_near(pt.point(), Point(100, 20))); + EXPECT_TRUE(are_near(pt.first.t, 0.5)); + EXPECT_TRUE(are_near(pt.second.asFlatTime(), 1.0)); + + // === Paths that split at a common node === + // Polylines + auto branch_90 = string_to_path("M 0,0 H 3 H 6 V 7"); + auto branch_45 = string_to_path("M 0,0 H 2 H 6 L 7,7"); + pt = parting_point(branch_90, branch_45); + EXPECT_TRUE(are_near(pt.point(), Point(6, 0))); + EXPECT_TRUE(are_near(pt.first.asFlatTime(), 2.0)); + EXPECT_TRUE(are_near(pt.second.asFlatTime(), 2.0)); + + // Arcs + auto quarter_circle_then_horiz = string_to_path("M 0,0 A 1,1 0,0,0 1,1 H 10"); + auto quarter_circle_then_slant = string_to_path("M 0,0 A 1,1 0,0,0 1,1 L 10, 1.1"); + pt = parting_point(quarter_circle_then_horiz, quarter_circle_then_slant); + EXPECT_TRUE(are_near(pt.point(), Point(1, 1))); + EXPECT_TRUE(are_near(pt.first.asFlatTime(), 1.0)); + EXPECT_TRUE(are_near(pt.second.asFlatTime(), 1.0)); + + // Last common nodes followed by degenerates + auto degen_horiz = string_to_path("M 0,0 A 1,1 0,0,0 1,1 V 1 H 1 L 1,1 H 10"); + auto degen_slant = string_to_path("M 0,0 A 1,1 0,0,0 1,1 V 1 H 1 L 1,1 L 10, 1.1"); + pt = parting_point(quarter_circle_then_horiz, quarter_circle_then_slant); + EXPECT_TRUE(are_near(pt.point(), Point(1, 1))); + + // === Paths that split at the starting point === + auto vertical = string_to_path("M 0,0 V 1"); + auto quarter = string_to_path("M 0,0 A 1,1 0,0,0, 1,1"); + pt = parting_point(vertical, quarter); + EXPECT_TRUE(are_near(pt.point(), Point(0, 0))); + EXPECT_TRUE(are_near(pt.first.asFlatTime(), 0.0)); + EXPECT_TRUE(are_near(pt.second.asFlatTime(), 0.0)); + + // === Symmetric split (both legs of the same length) === + auto left_leg = string_to_path("M 1,0 L 0,10"); + auto right_leg = string_to_path("M 1,0 L 2,10"); + pt = parting_point(left_leg, right_leg); + EXPECT_TRUE(are_near(pt.point(), Point(1, 0))); + EXPECT_TRUE(are_near(pt.first.asFlatTime(), 0.0)); + EXPECT_TRUE(are_near(pt.second.asFlatTime(), 0.0)); + + // === Different starting points === + auto start_at_0_0 = string_to_path("M 0,0 C 1,0 0,1 1,1"); + auto start_at_10_10 = string_to_path("M 10,10 L 50,50"); + pt = parting_point(start_at_0_0, start_at_10_10); + EXPECT_TRUE(are_near(pt.point(), Point (5,5))); + EXPECT_DOUBLE_EQ(pt.first.t, -1.0); + EXPECT_DOUBLE_EQ(pt.second.t, -1.0); + EXPECT_EQ(pt.first.curve_index, 0); + EXPECT_EQ(pt.second.curve_index, 0); +} + +TEST_F(PathTest, InitialFinalTangents) { + // Test tangents for an open path + auto L_shape = string_to_path("M 1,1 H 0 V 0"); + EXPECT_EQ(L_shape.initialUnitTangent(), Point(-1.0, 0.0)); + EXPECT_EQ(L_shape.finalUnitTangent(), Point(0.0, -1.0)); + + // Closed path with non-degenerate closing segment + auto triangle = string_to_path("M 0,0 H 2 L 0,3 Z"); + EXPECT_EQ(triangle.initialUnitTangent(), Point(1.0, 0.0)); + EXPECT_EQ(triangle.finalUnitTangent(), Point(0.0, -1.0)); + + // Closed path with a degenerate closing segment + auto full360 = string_to_path("M 0,0 A 1,1, 0,1,1, 0,2 A 1,1 0,1,1 0,0 Z"); + EXPECT_EQ(full360.initialUnitTangent(), Point(1.0, 0.0)); + EXPECT_EQ(full360.finalUnitTangent(), Point(1.0, 0.0)); + + // Test multiple degenerate segments at the start + auto start_degen = string_to_path("M 0,0 L 0,0 H 0 V 0 Q 1,0 1,1"); + EXPECT_EQ(start_degen.initialUnitTangent(), Point(1.0, 0.0)); + + // Test multiple degenerate segments at the end + auto end_degen = string_to_path("M 0,0 L 1,1 H 1 V 1 L 1,1"); + double comp = 1.0 / sqrt(2.0); + EXPECT_EQ(end_degen.finalUnitTangent(), Point(comp, comp)); + + // Test a long and complicated path with both tangents along the positive x-axis. + auto complicated = string_to_path("M 0,0 H 0 L 1,0 C 2,1 3,2 1,0 L 1,0 H 1 Q 2,3 0,5 H 2"); + EXPECT_EQ(complicated.initialUnitTangent(), Point(1.0, 0.0)); + EXPECT_EQ(complicated.finalUnitTangent(), Point(1.0, 0.0)); +} + +TEST_F(PathTest, WithoutDegenerates) { + // Ensure nothing changes when there are no degenerate segments to remove. + auto plain_open = string_to_path("M 0,0 Q 5,5 10,10"); + EXPECT_EQ(plain_open, plain_open.withoutDegenerateCurves()); + + auto closed_nondegen_closing = string_to_path("M 0,0 L 5,5 H 0 Z"); + EXPECT_EQ(closed_nondegen_closing,closed_nondegen_closing.withoutDegenerateCurves()); + + // Ensure that a degenerate closing segment is left alone. + auto closed_degen_closing = string_to_path("M 0,0 L 2,4 H 0 L 0,0 Z"); + EXPECT_EQ(closed_degen_closing, closed_degen_closing.withoutDegenerateCurves()); + + // Ensure that a trivial path is left alone (both open and closed). + auto trivial_open = string_to_path("M 0,0"); + EXPECT_EQ(trivial_open, trivial_open.withoutDegenerateCurves()); + + auto trivial_closed = string_to_path("M 0,0 Z"); + EXPECT_EQ(trivial_closed, trivial_closed.withoutDegenerateCurves()); + + // Ensure that initial degenerate segments are removed + auto degen_start = string_to_path("M 0,0 L 0,0 H 0 V 0 Q 5,5 10,10"); + auto degen_start_cleaned = degen_start.withoutDegenerateCurves(); + EXPECT_EQ(degen_start_cleaned, string_to_path("M 0,0 Q 5,5 10,10")); + EXPECT_NE(degen_start.size(), degen_start_cleaned.size()); + + // Ensure that degenerate segments are removed from the middle + auto degen_middle = string_to_path("M 0,0 L 1,1 H 1 V 1 L 1,1 Q 6,6 10,10"); + auto degen_middle_cleaned = degen_middle.withoutDegenerateCurves(); + EXPECT_EQ(degen_middle_cleaned, string_to_path("M 0,0 L 1,1 Q 6,6 10,10")); + EXPECT_NE(degen_middle.size(), degen_middle_cleaned.size()); + + // Ensure that degenerate segment are removed from the end of an open path + auto end_open = string_to_path("M 0,0 L 1,1 H 1 V 1 L 1,1"); + auto end_open_cleaned = end_open.withoutDegenerateCurves(); + EXPECT_EQ(end_open_cleaned, string_to_path("M 0,0 L 1,1")); + EXPECT_NE(end_open.size(), end_open_cleaned.size()); + + // Ensure removal of degenerates just before the closing segment + auto end_nondegen = string_to_path("M 0,0 L 1,1 L 0,1 H 0 V 1 Z"); + auto end_nondegen_cleaned = end_nondegen.withoutDegenerateCurves(); + EXPECT_EQ(end_nondegen_cleaned, string_to_path("M 0,0 L 1,1 L 0,1 Z")); + EXPECT_NE(end_nondegen.size(), end_nondegen_cleaned.size()); +} + +/** Test Path::extrema() */ +TEST_F(PathTest, GetExtrema) { + + // Circle of radius 4.5 centered at (-4.5, 0). + auto extrema_x = circle.extrema(X); + EXPECT_EQ(extrema_x.min_point, Point(-9, 0)); + EXPECT_EQ(extrema_x.max_point, Point( 0, 0)); + EXPECT_DOUBLE_EQ(extrema_x.min_time.asFlatTime(), 1.0); + EXPECT_DOUBLE_EQ(extrema_x.max_time.asFlatTime(), 0.0); + EXPECT_EQ(extrema_x.glance_direction_at_min, -1.0); + EXPECT_EQ(extrema_x.glance_direction_at_max, 1.0); + + auto extrema_y = circle.extrema(Y); + EXPECT_EQ(extrema_y.min_point, Point(-4.5, -4.5)); + EXPECT_EQ(extrema_y.max_point, Point(-4.5, 4.5)); + EXPECT_DOUBLE_EQ(extrema_y.min_time.asFlatTime(), 1.5); + EXPECT_DOUBLE_EQ(extrema_y.max_time.asFlatTime(), 0.5); + EXPECT_FLOAT_EQ(extrema_y.glance_direction_at_min, 1.0); + EXPECT_FLOAT_EQ(extrema_y.glance_direction_at_max, -1.0); + + // Positively oriented unit square + extrema_x = square.extrema(X); + EXPECT_DOUBLE_EQ(extrema_x.min_point[X], 0.0); + EXPECT_DOUBLE_EQ(extrema_x.max_point[X], 1.0); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_min, -1.0); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_max, 1.0); + + extrema_y = square.extrema(Y); + EXPECT_DOUBLE_EQ(extrema_y.min_point[Y], 0.0); + EXPECT_DOUBLE_EQ(extrema_y.max_point[Y], 1.0); + EXPECT_FLOAT_EQ(extrema_y.glance_direction_at_min, 1.0); + EXPECT_FLOAT_EQ(extrema_y.glance_direction_at_max, -1.0); + + // Path glancing its min X line while going towards negative Y + auto down_glance = string_to_path("M 1,18 L 0,0 1,-20"); + extrema_x = down_glance.extrema(X); + EXPECT_EQ(extrema_x.min_point, Point(0, 0)); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_min, -1.0); + EXPECT_DOUBLE_EQ(extrema_x.min_time.asFlatTime(), 1.0); + + // Similar but not at a node + auto down_glance_smooth = string_to_path("M 1,20 C 0,20 0,-20 1,-20"); + extrema_x = down_glance_smooth.extrema(X); + EXPECT_TRUE(are_near(extrema_x.min_point[Y], 0.0)); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_min, -1.0); + EXPECT_DOUBLE_EQ(extrema_x.min_time.asFlatTime(), 0.5); + + // Path coming down to the min X and then retreating horizontally + auto retreat = string_to_path("M 1,20 L 0,0 H 5 L 4,-20"); + extrema_x = retreat.extrema(X); + EXPECT_EQ(extrema_x.min_point, Point(0, 0)); + EXPECT_EQ(extrema_x.max_point, Point(5, 0)); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_min, -1.0); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_max, -1.0); + EXPECT_DOUBLE_EQ(extrema_x.min_time.asFlatTime(), 1.0); + EXPECT_DOUBLE_EQ(extrema_x.max_time.asFlatTime(), 2.0); + + // Perfectly horizontal path + auto horizontal = string_to_path("M 0,0 H 12"); + extrema_x = horizontal.extrema(X); + extrema_y = horizontal.extrema(Y); + EXPECT_EQ(extrema_x.min_point, Point(0, 0)); + EXPECT_EQ(extrema_x.max_point, Point(12, 0)); + EXPECT_DOUBLE_EQ(extrema_y.min_point[Y], 0.0); + EXPECT_DOUBLE_EQ(extrema_y.max_point[Y], 0.0); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_min, 0.0); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_max, 0.0); + EXPECT_FLOAT_EQ(extrema_y.glance_direction_at_min, 1.0); + EXPECT_FLOAT_EQ(extrema_y.glance_direction_at_max, 1.0); + EXPECT_DOUBLE_EQ(extrema_x.min_time.asFlatTime(), 0.0); + EXPECT_DOUBLE_EQ(extrema_x.max_time.asFlatTime(), 1.0); + + // Perfectly vertical path + auto vertical = string_to_path("M 0,0 V 42"); + extrema_y = vertical.extrema(Y); + extrema_x = vertical.extrema(X); + EXPECT_DOUBLE_EQ(extrema_x.min_point[Y], 0.0); + EXPECT_DOUBLE_EQ(extrema_x.max_point[Y], 0.0); + EXPECT_EQ(extrema_y.min_point, Point(0, 0)); + EXPECT_EQ(extrema_y.max_point, Point(0, 42)); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_min, 1.0); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_max, 1.0); + EXPECT_FLOAT_EQ(extrema_y.glance_direction_at_min, 0.0); + EXPECT_FLOAT_EQ(extrema_y.glance_direction_at_max, 0.0); + EXPECT_DOUBLE_EQ(extrema_y.min_time.asFlatTime(), 0.0); + EXPECT_DOUBLE_EQ(extrema_y.max_time.asFlatTime(), 1.0); + + // Detect downward glance at the closing point (degenerate closing segment) + auto closed = string_to_path("M 0,0 L 1,-2 H 3 V 5 H 1 L 0,0 Z"); + extrema_x = closed.extrema(X); + EXPECT_EQ(extrema_x.min_point, Point(0, 0)); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_min, -1.0); + + // Same but with a non-degenerate closing segment + auto closed_nondegen = string_to_path("M 0,0 L 1,-2 H 3 V 5 H 1 Z"); + extrema_x = closed_nondegen.extrema(X); + EXPECT_EQ(extrema_x.min_point, Point(0, 0)); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_min, -1.0); + + // Collapsed Bezier not glancing up nor down + auto collapsed = string_to_path("M 10, 0 Q -10 0 10, 0"); + extrema_x = collapsed.extrema(X); + EXPECT_EQ(extrema_x.min_point, Point(0, 0)); + EXPECT_EQ(extrema_x.max_point, Point(10, 0)); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_min, 0.0); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_max, 0.0); + + // Degenerate segments at min X + auto degen = string_to_path("M 0.01,20 L 0, 0 H 0 V 0 L 0,0 V 0 L 0.02 -30"); + extrema_x = degen.extrema(X); + EXPECT_EQ(extrema_x.min_point, Point(0, 0)); + EXPECT_FLOAT_EQ(extrema_x.glance_direction_at_min, -1.0); +} + +/** Regression test for issue https://gitlab.com/inkscape/lib2geom/-/issues/50 */ +TEST_F(PathTest, PizzaSlice) +{ + auto pv = parse_svg_path("M 0 0 L 0.30901699437494745 0.9510565162951535 " + "A 1 1 0 0 1 -0.8090169943749473 0.5877852522924732 z"); + auto §or = pv[0]; + Path piece; + EXPECT_NO_THROW(piece = sector.portion(PathTime(0, 0.0), PathTime(2, 0.0), false)); + EXPECT_FALSE(piece.closed()); + EXPECT_TRUE(piece.size() == 2 || + (piece.size() == 3 && piece[2].isDegenerate())); + EXPECT_EQ(piece.finalPoint(), Point(-0.8090169943749473, 0.5877852522924732)); + + // Test slicing in the middle of an arc and past its end + pv = parse_svg_path("M 0,0 H 1 A 1,1 0 0 1 0.3080657835086775,0.9513650577098072 z"); + EXPECT_NO_THROW(piece = pv[0].portion(PathTime(1, 0.5), PathTime(2, 1.0))); + EXPECT_FALSE(piece.closed()); + EXPECT_EQ(piece.finalPoint(), pv[0].finalPoint()); + + // Test slicing from before the start to a point on the arc + EXPECT_NO_THROW(piece = pv[0].portion(PathTime(0, 0.5), PathTime(1, 0.5))); + EXPECT_FALSE(piece.closed()); + EXPECT_EQ(piece.initialPoint(), pv[0].pointAt(PathTime(0, 0.5))); + EXPECT_EQ(piece.finalPoint(), pv[0].pointAt(PathTime(1, 0.5))); + + // Test slicing a part of the arc + EXPECT_NO_THROW(piece = pv[0].portion(PathTime(1, 0.25), PathTime(1, 0.75))); + EXPECT_FALSE(piece.closed()); + EXPECT_EQ(piece.size(), 1); + + // Test slicing in reverse + EXPECT_NO_THROW(piece = pv[0].portion(PathTime(2, 1.0), PathTime(1, 0.5))); + EXPECT_FALSE(piece.closed()); + EXPECT_EQ(piece.finalPoint(), pv[0].pointAt(PathTime(1, 0.5))); + + EXPECT_NO_THROW(piece = pv[0].portion(PathTime(1, 0.5), PathTime(0, 0.5))); + EXPECT_FALSE(piece.closed()); + EXPECT_EQ(piece.initialPoint(), pv[0].pointAt(PathTime(1, 0.5))); + EXPECT_EQ(piece.finalPoint(), pv[0].pointAt(PathTime(0, 0.5))); + + EXPECT_NO_THROW(piece = pv[0].portion(PathTime(1, 0.75), PathTime(1, 0.25))); + EXPECT_FALSE(piece.closed()); + EXPECT_EQ(piece.size(), 1); +} + +/* + 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 : diff --git a/tests/pick.h b/tests/pick.h new file mode 100644 index 0000000..3e43bd5 --- /dev/null +++ b/tests/pick.h @@ -0,0 +1,172 @@ +/* + * Routines for generating anything randomly + * + * Authors: + * Marco Cecchetti <mrcekets at gmail.com> + * + * Copyright 2008 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. + */ + +#ifndef _GEOM_SL_PICK_H_ +#define _GEOM_SL_PICK_H_ + + +#include <2geom/symbolic/multipoly.h> +#include <2geom/symbolic/matrix.h> + +inline +size_t pick_uint(size_t max) +{ + return (std::rand() % (max+1)); +} + +inline +int pick_int(size_t max) +{ + int s = pick_uint(2); + if (s == 0) s = -1; + return s * (std::rand() % (max+1)); +} + +inline +Geom::SL::multi_index_type pick_multi_index(size_t N, size_t max) +{ + Geom::SL::multi_index_type I(N); + for (size_t i = 0; i < I.size(); ++i) + I[i] = pick_uint(max); + return I; +} + +template <size_t N> +inline +typename Geom::SL::mvpoly<N, double>::type +pick_polyN(size_t d, size_t m) +{ + typename Geom::SL::mvpoly<N, double>::type p; + size_t d0 = pick_uint(d); + for (size_t i = 0; i <= d0; ++i) + { + p.coefficient(i, pick_polyN<N-1>(d, m)); + } + return p; +} + +template <> +inline +double pick_polyN<0>(size_t /*d*/, size_t m) +{ + return pick_int(m); +} + + +template <size_t N> +inline +typename Geom::SL::mvpoly<N, double>::type +pick_poly_max(size_t d, size_t m) +{ + typename Geom::SL::mvpoly<N, double>::type p; + for (size_t i = 0; i <= d; ++i) + { + p.coefficient(i, pick_poly_max<N-1>(d-i, m)); + } + return p; +} + +template <> +inline +double pick_poly_max<0>(size_t /*d*/, size_t m) +{ + return pick_int(m); +} + + +template <size_t N> +inline +Geom::SL::MultiPoly<N, double> +pick_multipoly(size_t d, size_t m) +{ + return Geom::SL::MultiPoly<N, double>(pick_polyN<N>(d, m)); +} + +template <size_t N> +inline +Geom::SL::MultiPoly<N, double> +pick_multipoly_max(size_t d, size_t m) +{ + return Geom::SL::MultiPoly<N, double>(pick_poly_max<N>(d, m)); +} + + + +inline +Geom::SL::Matrix< Geom::SL::MultiPoly<2, double> > +pick_matrix(size_t n, size_t d, size_t m) +{ + Geom::SL::Matrix< Geom::SL::MultiPoly<2, double> > M(n, n); + for (size_t i = 0; i < n; ++i) + { + for (size_t j = 0; j < n; ++j) + { + M(i,j) = pick_multipoly_max<2>(d, m); + } + } + return M; +} + + +inline +Geom::SL::Matrix< Geom::SL::MultiPoly<2, double> > +pick_symmetric_matrix(size_t n, size_t d, size_t m) +{ + Geom::SL::Matrix< Geom::SL::MultiPoly<2, double> > M(n, n); + for (size_t i = 0; i < n; ++i) + { + for (size_t j = 0; j < i; ++j) + { + M(i,j) = M(j,i) = pick_multipoly_max<2>(d, m); + } + } + for (size_t i = 0; i < n; ++i) + { + M(i,i) = pick_multipoly_max<2>(d, m); + } + return M; +} + + +#endif // _GEOM_SL_PICK_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 : diff --git a/tests/planar-graph-test.cpp b/tests/planar-graph-test.cpp new file mode 100644 index 0000000..f19e2eb --- /dev/null +++ b/tests/planar-graph-test.cpp @@ -0,0 +1,457 @@ +/** @file + * @brief Unit tests for PlanarGraph class template + */ +/* + * 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. + */ + +#include <gtest/gtest.h> +#include <iostream> + +#include <2geom/point.h> +#include <2geom/pathvector.h> +#include <2geom/svg-path-parser.h> +#include <2geom/svg-path-writer.h> + +#include "planar-graph.h" +#include "testing.h" + +using namespace Geom; + +#define PV(d) (parse_svg_path(d)) +#define PTH(d) (std::move(PV(d)[0])) +#define REV(d) ((PV(d)[0]).reversed()) + +/** An edge label for the purpose of tests. */ +struct TestLabel +{ + unsigned reversal_count = 0, merge_count = 0, detachment_count = 0; + void onReverse() { reversal_count++; } + void onMergeWith(TestLabel const &) { merge_count++; } + void onDetach() { detachment_count++; } +}; + +using TestGraph = PlanarGraph<TestLabel>; + +static std::vector<TestLabel> extract_labels(TestGraph const &graph) +{ + // Find labels of edges remaining in the graph. + std::vector<TestLabel> result; + for (auto &e : graph.getEdges()) { + if (!e.detached) { + result.push_back(e.label); + } + } + return result; +} + +class PlanarGraphTest : public ::testing::Test +{ +}; + +/** Test edge insertion and vertex clumping to within the tolerance. */ +TEST(PlanarGraphTest, EdgeInsertion) +{ + double const precision = 1e-3; + auto graph = TestGraph(precision); + graph.insertEdge(PTH("M 0, 0 L 1, 0")); + graph.insertEdge(PTH("M 0, 1 L 1, 1")); // } Endpoints near + graph.insertEdge(PTH("M 1, 0 L 1, 1.0009")); // } but not exact. + + auto vertices = graph.getVertices(); + + // Test vertex clumping within the given precision + EXPECT_EQ(vertices.size(), 4); + EXPECT_EQ(graph.numEdges(), 3); + + // Test lexicographic vertex position sorting by X and then Y + EXPECT_EQ(vertices.front().point(), Point(0, 0)); + auto after = std::next(vertices.begin()); + EXPECT_EQ(after->point(), Point(0, 1)); + ++after; + EXPECT_EQ(after->point(), Point(1, 0)); + EXPECT_TRUE(are_near(vertices.back().point(), Point(1, 1), precision)); + + EXPECT_FALSE(graph.isRegularized()); +} + +/** Test PlanarGraph<T>::insertDetached(). */ +TEST(PlanarGraphTest, InsertDetached) +{ + TestGraph graph; + auto detached = graph.insertDetached(PTH("M 0,0 A 1,1 0,0,1 2,0 V -2 H 0 Z")); + + auto const &edges = graph.getEdges(); + EXPECT_EQ(edges.size(), 1); + EXPECT_TRUE(edges.at(detached).detached); + EXPECT_TRUE(edges.at(detached).inserted_as_detached); + + EXPECT_EQ(graph.numVertices(), 0); + EXPECT_EQ(graph.numEdges(false), 0); + EXPECT_TRUE(graph.isRegularized()); +} + +/** Test signed area calculation. */ +TEST(PlanarGraphTest, ClosedPathArea) +{ + // Square with counter-clockwise oriented boundary, when imagining that the y-axis + // points up – expect the area to be +1. + auto square_positive = PTH("M 0,0 H 1 V 1 H 0 Z"); + EXPECT_DOUBLE_EQ(TestGraph::closedPathArea(square_positive), 1.0); + + // Expect negative area for a negatively oriented path. + auto triangle_negative = PTH("M 0,0 V 1 L 1,1 Z"); + EXPECT_DOUBLE_EQ(TestGraph::closedPathArea(triangle_negative), -0.5); +} + +/** Test the detection of direction of deviation of initially tangent paths. */ +TEST(PlanarGraphTest, Deviation) +{ + auto vertical_up = PTH("M 0,0 V 1"); + auto arc_right1 = PTH("M 0,0 A 1,1 0,1,0 2,0"); + auto arc_left1 = PTH("M 0,0 A 1,1 0,1,1 -2,0"); + auto arc_right2 = PTH("M 0,0 A 2,2 0,1,0, 4,0"); + auto arc_left2 = PTH("M 0,0 A 2,2 0,1,1 -4,0"); + // A very "flat" Bézier curve deviating to the right but slower than the large arc + auto bezier_right = PTH("M 0,0 C 0,50 1,20 2,10"); + + EXPECT_TRUE(TestGraph::deviatesLeft(arc_left1, arc_left2)); + EXPECT_TRUE(TestGraph::deviatesLeft(arc_left2, vertical_up)); + EXPECT_TRUE(TestGraph::deviatesLeft(vertical_up, arc_right2)); + EXPECT_TRUE(TestGraph::deviatesLeft(vertical_up, bezier_right)); + EXPECT_TRUE(TestGraph::deviatesLeft(bezier_right, arc_right2)); + EXPECT_TRUE(TestGraph::deviatesLeft(arc_right2, arc_right1)); + EXPECT_TRUE(TestGraph::deviatesLeft(arc_left1, arc_right1)); + EXPECT_TRUE(TestGraph::deviatesLeft(arc_left2, arc_right1)); + + EXPECT_FALSE(TestGraph::deviatesLeft(arc_right1, vertical_up)); + EXPECT_FALSE(TestGraph::deviatesLeft(arc_right1, arc_right2)); + EXPECT_FALSE(TestGraph::deviatesLeft(vertical_up, arc_left2)); + EXPECT_FALSE(TestGraph::deviatesLeft(arc_left2, arc_left1)); + EXPECT_FALSE(TestGraph::deviatesLeft(arc_right1, arc_left1)); + EXPECT_FALSE(TestGraph::deviatesLeft(arc_right1, arc_left2)); +} + +/** Test sorting of incidences at a vertex by the outgoing heading. */ +TEST(PlanarGraphTest, BasicAzimuthalSort) +{ + TestGraph graph; + + // Imagine the Y-axis pointing up (as in mathematics)! + bool const clockwise = true; + unsigned const num_rays = 9; + unsigned edges[num_rays]; + + // Insert the edges randomly but store them in what we know to be the + // clockwise order of outgoing azimuths from the vertex at the origin. + edges[7] = graph.insertEdge(PTH("M -0.2, -1 L 0, 0")); + edges[1] = graph.insertEdge(PTH("M -1, 0.2 L 0, 0")); + edges[4] = graph.insertEdge(PTH("M 0, 0 L 1, 0.2")); + edges[6] = graph.insertEdge(PTH("M 0.1, -1 L 0, 0")); + edges[2] = graph.insertEdge(PTH("M 0, 0 L -0.3, 1")); + edges[0] = graph.insertEdge(PTH("M -1, 0 H 0")); + edges[5] = graph.insertEdge(PTH("M 0, 0 L 1, -0.2")); + edges[3] = graph.insertEdge(PTH("M 0.2, 1 L 0, 0")); + edges[8] = graph.insertEdge(PTH("M -1, -0.1 L 0, 0")); + + // We expect the incidence to edges[0] to be the last one + // in the sort order so it should appear first when going clockwise. + auto [origin, incidence] = graph.getIncidence(edges[0], TestGraph::Incidence::END); + ASSERT_TRUE(origin); + ASSERT_TRUE(incidence); + + // Expect ±pi as the azimuth + EXPECT_DOUBLE_EQ(std::abs(incidence->azimuth), M_PI); + + // Test sort order + for (unsigned i = 0; i < num_rays; i++) { + EXPECT_EQ(incidence->index, edges[i]); + incidence = (TestGraph::Incidence *)&graph.nextIncidence(*origin, *incidence, clockwise); + } +} + +/** Test retrieval of a path inserted as an edge in both orientations. */ +TEST(PlanarGraphTest, PathRetrieval) +{ + TestGraph graph; + + Path const path = PTH("M 0,0 L 1,1 C 2,2 4,2 5,1"); + Path const htap = path.reversed(); + + auto edge = graph.insertEdge(path); + + ASSERT_EQ(graph.numEdges(), 1); + + auto [start_point, start_incidence] = graph.getIncidence(edge, TestGraph::Incidence::START); + ASSERT_TRUE(start_point); + ASSERT_TRUE(start_incidence); + EXPECT_EQ(graph.getOutgoingPath(start_incidence), path); + EXPECT_EQ(graph.getIncomingPath(start_incidence), htap); + + auto [end_point, end_incidence] = graph.getIncidence(edge, TestGraph::Incidence::END); + ASSERT_TRUE(end_point); + ASSERT_TRUE(end_incidence); + EXPECT_EQ(graph.getIncomingPath(end_incidence), path); + EXPECT_EQ(graph.getOutgoingPath(end_incidence), htap); +} + +/** Make sure the edge labels are correctly stored. */ +TEST(PlanarGraphTest, LabelRetrieval) +{ + TestGraph graph; + TestLabel label; + + label.reversal_count = 420; + label.merge_count = 69; + label.detachment_count = 111; + + auto edge = graph.insertEdge(PTH("M 0,0 L 1,1"), std::move(label)); + + auto retrieved = graph.getEdge(edge).label; + EXPECT_EQ(retrieved.reversal_count, 420); + EXPECT_EQ(retrieved.merge_count, 69); + EXPECT_EQ(retrieved.detachment_count, 111); +} + +/** Regularization of duplicate edges. */ +TEST(PlanarGraphTest, MergeDuplicate) +{ + char const *const d = "M 2, 3 H 0 C 1,4 1,5 0,6 H 10 L 8, 0"; + char const *const near_d = "M 2.0009,3 H 0 C 1,4 1,5 0,6 H 10.0009 L 8, 0.0005"; + + // Test removal of perfect overlap: + TestGraph graph; + graph.insertEdge(PTH(d)); + graph.insertEdge(PTH(d)); // exact duplicate + graph.regularize(); + + EXPECT_TRUE(graph.isRegularized()); + + auto remaining = extract_labels(graph); + + // Expect there to be only 1 edge after regularization. + ASSERT_EQ(remaining.size(), 1); + + EXPECT_EQ(remaining[0].merge_count, 1); // expect one merge, + EXPECT_EQ(remaining[0].reversal_count, 0); // no reversals, + EXPECT_EQ(remaining[0].detachment_count, 0); // no detachments. + + // Test removal of imperfect overlaps within numerical precision + TestGraph fuzzy{1e-3}; + fuzzy.insertEdge(PTH(d)); + fuzzy.insertEdge(PTH(near_d)); + fuzzy.regularize(); + + EXPECT_TRUE(fuzzy.isRegularized()); + + auto fuzmaining = extract_labels(fuzzy); + ASSERT_EQ(fuzmaining.size(), 1); + + EXPECT_EQ(fuzmaining[0].merge_count, 1); // expect one merge, + EXPECT_EQ(fuzmaining[0].reversal_count, 0); // no reversals, + EXPECT_EQ(fuzmaining[0].detachment_count, 0); // no detachments. + + // Test overlap of edges with oppositie orientations. + TestGraph twoway; + twoway.insertEdge(PTH(d)); + twoway.insertEdge(REV(d)); + twoway.regularize(); + + EXPECT_TRUE(twoway.isRegularized()); + + auto left = extract_labels(twoway); + ASSERT_EQ(left.size(), 1); + + EXPECT_EQ(left[0].merge_count, 1); // expect one merge, + EXPECT_TRUE(left[0].reversal_count == 0 || left[0].reversal_count == 1); // 0 or 1 reversals + EXPECT_EQ(left[0].detachment_count, 0); // no detachments. +} + +/** Regularization of a shorter edge overlapping a longer one. */ +TEST(PlanarGraphTest, MergePartial) +{ + TestGraph graph; + auto longer = graph.insertEdge(PTH("M 0, 0 L 10, 10")); + auto shorter = graph.insertEdge(PTH("M 0, 0 L 6, 6")); + + EXPECT_EQ(graph.numVertices(), 3); + + graph.regularize(); + + EXPECT_EQ(graph.numVertices(), 3); + EXPECT_TRUE(graph.isRegularized()); + + auto labels = extract_labels(graph); + ASSERT_EQ(labels.size(), 2); + + EXPECT_EQ(labels[longer].merge_count, 0); + EXPECT_EQ(labels[longer].reversal_count, 0); + EXPECT_EQ(labels[longer].detachment_count, 0); + + EXPECT_EQ(labels[shorter].merge_count, 1); + EXPECT_EQ(labels[shorter].reversal_count, 0); + EXPECT_EQ(labels[shorter].detachment_count, 0); + + // Now the same thing but with edges of opposite orientations. + TestGraph graphopp; + longer = graphopp.insertEdge(PTH("M 0, 0 L 10, 0")); + shorter = graphopp.insertEdge(PTH("M 10, 0 L 5, 0")); + + EXPECT_EQ(graphopp.numVertices(), 3); + + graphopp.regularize(); + + EXPECT_EQ(graphopp.numVertices(), 3); + EXPECT_TRUE(graphopp.isRegularized()); + + labels = extract_labels(graphopp); + ASSERT_EQ(labels.size(), 2); + + EXPECT_EQ(labels[longer].merge_count, 0); + EXPECT_EQ(labels[longer].reversal_count, 0); + EXPECT_EQ(labels[longer].detachment_count, 0); + + EXPECT_EQ(labels[shorter].merge_count, 1); + EXPECT_EQ(labels[shorter].reversal_count, 0); + EXPECT_EQ(labels[shorter].detachment_count, 0); +} + +/** Regularization of a Y-split. */ +TEST(PlanarGraphTest, MergeY) +{ + TestGraph graph; + auto left = graph.insertEdge(PTH("M 1 0 V 1 L 0, 2")); + auto right = graph.insertEdge(PTH("M 1,0 V 1 L 2, 2")); + + EXPECT_EQ(graph.numVertices(), 3); + graph.regularize(); + EXPECT_EQ(graph.numVertices(), 4); + + auto edges = graph.getEdges(); + EXPECT_EQ(edges.size(), 3); + + EXPECT_TRUE(are_near(edges[right].start->point(), Point(1, 1))); +} + +/** Test reversal of a wrongly oriented teardrop */ +TEST(PlanarGraphTest, Teardrop) +{ + TestGraph graph; + auto loop = graph.insertEdge(PTH("M 1,0 A 1,1, 0,0,1 0,1 L 2,2 V 1 H 1 V 0")); + // Insert a few unrelated edges + auto before = graph.insertEdge(PTH("M 1,0 H 10")); + auto after = graph.insertEdge(PTH("M 1,0 H -10")); + + EXPECT_EQ(graph.numVertices(), 3); + + graph.regularize(); + + EXPECT_EQ(graph.numVertices(), 3); + auto [start_vertex, start_incidence] = graph.getIncidence(loop, TestGraph::Incidence::START); + auto [end_vertex, end_incidence] = graph.getIncidence(loop, TestGraph::Incidence::END); + + EXPECT_EQ(start_vertex, end_vertex); + ASSERT_NE(start_vertex, nullptr); + + // Check that the incidences have been swapped + EXPECT_EQ(start_vertex->cyclicNextIncidence(end_incidence), start_incidence); + EXPECT_EQ(start_vertex->cyclicPrevIncidence(start_incidence), end_incidence); + auto [b, before_incidence] = graph.getIncidence(before, TestGraph::Incidence::START); + EXPECT_EQ(start_vertex->cyclicNextIncidence(before_incidence), end_incidence); + auto [a, after_incidence] = graph.getIncidence(after, TestGraph::Incidence::START); + EXPECT_EQ(start_vertex->cyclicPrevIncidence(after_incidence), start_incidence); +} + +/** Test the regularization of a lasso-shaped path. */ +TEST(PlanarGraphTest, ReglueLasso) +{ + TestGraph graph; + // Insert a lasso-shaped path (a teardrop with initial self-overlap). + auto original_lasso = graph.insertEdge(PTH("M 0,0 V 1 C 0,2 1,3 1,4 " + "A 1,1 0,1,1 -1,4 C -1,3 0,2 0,1 V 0")); + EXPECT_EQ(graph.numVertices(), 1); + + graph.regularize(); + EXPECT_EQ(graph.numVertices(), 2); + EXPECT_EQ(graph.numEdges(false), 2); + EXPECT_TRUE(graph.getEdge(original_lasso).detached); + + auto const &edges = graph.getEdges(); + // Find the edge from origin and ensure it got glued. + auto from_origin = std::find_if(edges.begin(), edges.end(), [](auto const &edge) -> bool { + return !edge.detached && (edge.start->point() == Point(0, 0) || + edge.end->point() == Point(0, 0)); + }); + ASSERT_NE(from_origin, edges.end()); + ASSERT_EQ(from_origin->label.merge_count, 1); +} + +/** Test the removal of a collapsed loop. */ +TEST(PlanarGraphTest, RemoveCollapsed) +{ + TestGraph graph; + // Insert a collapsed loop + auto collapsed = graph.insertEdge(PTH("M 0,0 L 1,1 L 0,0")); + ASSERT_EQ(graph.numEdges(), 1); + graph.regularize(); + ASSERT_EQ(graph.numEdges(false), 0); + ASSERT_TRUE(graph.getEdge(collapsed).detached); + + TestGraph fuzzy(1e-3); + // Insert a nearly collapsed loop + auto nearly = fuzzy.insertEdge(PTH("M 0,0 H 2 V 0.001 L 1,0 H 0")); + ASSERT_EQ(fuzzy.numEdges(), 1); + fuzzy.regularize(); + ASSERT_EQ(fuzzy.numEdges(false), 0); + ASSERT_TRUE(fuzzy.getEdge(nearly).detached); +} + +/** Test regularization of straddling runs. */ +TEST(PlanarGraphTest, RemoveWisp) +{ + TestGraph graph; + // Insert a horizontal segment at the origin towards positive X: + graph.insertEdge(PTH("M 0 0 H 1")); + // Insert a path with a collapsed Bézier curve towards negative X: + graph.insertEdge(PTH("M 0 0 C -1 0 -1 0 0 0")); + graph.regularize(); + + // Ensure that the folded Bézier is removed (and no segfault occurs). + EXPECT_EQ(graph.numEdges(false), 1); +} +/* + 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 : diff --git a/tests/point-test.cpp b/tests/point-test.cpp new file mode 100644 index 0000000..16596a5 --- /dev/null +++ b/tests/point-test.cpp @@ -0,0 +1,119 @@ +/** @file + * @brief Unit tests for Point, IntPoint and related functions. + * Uses the Google Testing Framework + *//* + * Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright 2014-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 <gtest/gtest.h> +#include <2geom/point.h> + +namespace Geom { + +TEST(PointTest, Normalize) { + Point a(1e-18, 0); + Point b = a; + a.normalize(); + + EXPECT_EQ(a, Point(1, 0)); + EXPECT_EQ(b.normalized(), a); + EXPECT_NE(b, a); +} + +TEST(PointTest, ScalarOps) { + Point a(1,2); + EXPECT_EQ(a * 2, Point(2, 4)); + EXPECT_EQ(2 * a, Point(2, 4)); + EXPECT_EQ(a / 2, Point(0.5, 1)); + + Point b = a; + a *= 2; + a /= 2; + EXPECT_EQ(a, b); +} + +TEST(PointTest, Rounding) { + Point a(-0.7, 0.7); + IntPoint aceil(0, 1), afloor(-1, 0), around(-1, 1); + EXPECT_TRUE(a.ceil() == aceil); + EXPECT_TRUE(a.floor() == afloor); + EXPECT_TRUE(a.round() == around); +} + +TEST(PointTest, Near) { + EXPECT_TRUE(are_near(Point(), Point(0, 1e-6))); + EXPECT_FALSE(are_near(Point(), Point(0, 1e-4))); + + EXPECT_TRUE(are_near_rel(Point(100, 0), Point(100, 1e-4))); + EXPECT_FALSE(are_near_rel(Point(100, 0), Point(100, 1e-2))); +} + +TEST(PointTest, Multiplicative) { + EXPECT_EQ(Point(2, 3) * Point(4, 5), Point(8, 15)); + EXPECT_EQ(IntPoint(2, 3) * IntPoint(4, 5), IntPoint(8, 15)); + EXPECT_EQ(Point(10, 11) / Point(2, 3), Point(5, 11.0 / 3.0)); + EXPECT_EQ(IntPoint(10, 11) / IntPoint(2, 3), IntPoint(5, 11 / 3)); +} + +TEST(PointTest, PointCtors) { + Point a(2, 3); + EXPECT_EQ(a[X], 2); + EXPECT_EQ(a[Y], 3); + + a.~Point(); + new (&a) Point; + EXPECT_EQ(a, Point(0, 0)); + + a = Point(IntPoint(4, 5)); + EXPECT_EQ(a[X], 4); + EXPECT_EQ(a[Y], 5); +} + +TEST(PointTest, IntPointCtors) { + IntPoint a(2, 3); + EXPECT_EQ(a[X], 2); + EXPECT_EQ(a[Y], 3); + + a.~IntPoint(); + new (&a) IntPoint; + EXPECT_EQ(a, IntPoint(0, 0)); +} + +} // end 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 : diff --git a/tests/polybez-cases.svg b/tests/polybez-cases.svg new file mode 100644 index 0000000..1fa653a --- /dev/null +++ b/tests/polybez-cases.svg @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="1024" + height="768" + id="svg2" + version="1.1" + inkscape:version="0.47 r22583" + sodipodi:docname="polybez-cases.svg"> + <defs + id="defs4"> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 526.18109 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="744.09448 : 526.18109 : 1" + inkscape:persp3d-origin="372.04724 : 350.78739 : 1" + id="perspective10" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="11.2" + inkscape:cx="338.56564" + inkscape:cy="493.11416" + inkscape:document-units="px" + inkscape:current-layer="sweep1" + showgrid="true" + inkscape:snap-grids="true" + inkscape:window-width="1440" + inkscape:window-height="825" + inkscape:window-x="0" + inkscape:window-y="24" + inkscape:window-maximized="1"> + <inkscape:grid + type="xygrid" + id="grid2820" + empspacing="5" + visible="true" + enabled="true" + snapvisiblegridlinesonly="false" /> + </sodipodi:namedview> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-284.36218)"> + <g + id="g2829"> + <path + id="path2824" + d="m 335,532 0,-15 10,-10 10,0 0,12 -7,0 0,7 7,0 0,6 -20,0 z" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + id="path2826" + d="m 338,524 0,-10 5,-5 12,0 0,15 -17,0 z" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + </g> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 370,507 15,0 -15,20 15,0 -15,-20 z" + id="path2828" /> + <g + id="g2836"> + <path + id="path2830" + d="m 395,507 0,20 15,0 0,-20 -15,0 z" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + id="path2832" + d="m 400,512 0,10 5,0 0,-10 -5,0 z" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + </g> + <g + id="g2840"> + <path + id="path2834" + d="m 420,507 0,20 10,0 0,-20 -10,0 z" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + id="path2838" + d="m 420,512 10,0 0,10 -10,0 0,-10 z" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + </g> + <g + id="g2844"> + <path + transform="translate(0,284.36218)" + id="path2861" + d="m 335,273 5,-10 5,0 -5,10 -5,0 z" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,284.36218)" + id="path2863" + d="m 335,263 5,10 5,0 -5,-10 -5,0 z" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + </g> + <g + id="g2848"> + <path + transform="translate(0,284.36218)" + id="path2865" + d="m 355,273 c 0,-10 5,-15 15,-15 10,0 15,15 5,15 -10,0 -19.82143,0 -20,0 z" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + transform="translate(0,284.36218)" + id="path2867" + d="m 355,258 15,15 5,0 -15,-15 -5,0 z" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + </g> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 390,273 c 0,0 0,-20 0,-15 0,5 5,-5 5,-5 l 5,0 0,20 -10,0 z" + id="path2869" + transform="translate(0,284.36218)" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 405,273 c 0,0 0,-10 0,-15 0,-5 0,-5 5,-5 5,0 5,0 5,5 0,5 -10,14.82143 -10,15 z" + id="path2871" + transform="translate(0,284.36218)" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 339.46429,565.30861 c -4.5365,-2.22147 -11.77743,5.69134 -5.21169,7.66819 2.96423,0.36599 10.83771,-2.10029 8.78922,3.46024 -1.13154,2.4938 -6.48404,5.18119 -7.60122,3.87235 4.97889,-1.66476 5.51215,4.06019 3.64354,6.27906 2.59991,2.27135 9.90909,-2.08687 10.22789,-0.44567 -5.83309,2.53694 -12.2305,3.77526 -18.58139,3.51925 -5.55304,-1.87666 0.0508,-8.52008 3.64017,-9.2859 3.14953,-0.57328 11.95334,1.68857 9.98614,-3.95731 -2.41191,-3.92307 -8.83522,2.26895 -9.24309,-2.87701 1.90388,-1.51652 8.28914,-1.45159 6.19223,-5.58351 -2.15792,-0.14937 -0.81405,-2.34484 -1.8418,-2.64969 z" + id="path2873" /> + <g + id="sweep1"> + <path + id="path2892" + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 307,259 c 0,0 8,8 -3,11 -5,1 -5,9 -5,9 m 4,-17 1,3 m -6,-8 3,12 m -9,-11 1,4 2,3 -1,4 3,3 -2,5 m -9,-19 4,14" + transform="translate(0,284.36218)" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 296,280 5,-11" + id="path2908" + transform="translate(0,284.36218)" /> + <path + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 309,283 -5,-18" + id="path2904" + transform="translate(0,284.36218)" /> + </g> + </g> +</svg> diff --git a/tests/polynomial-test.cpp b/tests/polynomial-test.cpp new file mode 100644 index 0000000..699820a --- /dev/null +++ b/tests/polynomial-test.cpp @@ -0,0 +1,126 @@ +/** @file + * @brief Unit tests for Polynomial and related functions. + * Uses the Google Testing Framework + *//* + * Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright 2015-2019 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 "testing.h" +#include <iostream> +#include <glib.h> + +#include <2geom/polynomial.h> + +using namespace Geom; + +TEST(PolynomialTest, SolveQuadratic) { + for (unsigned i = 0; i < 1000; ++i) { + Coord x1 = g_random_double_range(-100, 100); + Coord x2 = g_random_double_range(-100, 100); + + Coord a = g_random_double_range(-10, 10); + Coord b = -a * (x1 + x2); + Coord c = a * x1 * x2; + + std::vector<Coord> result = solve_quadratic(a, b, c); + + EXPECT_EQ(result.size(), 2u); + if (x1 < x2) { + EXPECT_FLOAT_EQ(result[0], x1); + EXPECT_FLOAT_EQ(result[1], x2); + } else { + EXPECT_FLOAT_EQ(result[0], x2); + EXPECT_FLOAT_EQ(result[1], x1); + } + } +} + +TEST(PolynomialTest, SolvePathologicalQuadratic) { + std::vector<Coord> r; + + r = solve_quadratic(1, -1e9, 1); + ASSERT_EQ(r.size(), 2u); + EXPECT_FLOAT_EQ(r[0], 1e-9); + EXPECT_FLOAT_EQ(r[1], 1e9); + + r = solve_quadratic(1, -4, 3.999999); + ASSERT_EQ(r.size(), 2u); + EXPECT_FLOAT_EQ(r[0], 1.999); + EXPECT_FLOAT_EQ(r[1], 2.001); + + r = solve_quadratic(1, 0, -4); + ASSERT_EQ(r.size(), 2u); + EXPECT_FLOAT_EQ(r[0], -2); + EXPECT_FLOAT_EQ(r[1], 2); + + r = solve_quadratic(1, 0, -16); + ASSERT_EQ(r.size(), 2u); + EXPECT_FLOAT_EQ(r[0], -4); + EXPECT_FLOAT_EQ(r[1], 4); + + r = solve_quadratic(1, 0, -100); + ASSERT_EQ(r.size(), 2u); + EXPECT_FLOAT_EQ(r[0], -10); + EXPECT_FLOAT_EQ(r[1], 10); +} + +TEST(PolynomialTest, SolveCubic) { + for (unsigned i = 0; i < 1000; ++i) { + Coord x1 = g_random_double_range(-100, 100); + Coord x2 = g_random_double_range(-100, 100); + Coord x3 = g_random_double_range(-100, 100); + + Coord a = g_random_double_range(-10, 10); + Coord b = -a * (x1 + x2 + x3); + Coord c = a * (x1*x2 + x2*x3 + x1*x3); + Coord d = -a * x1 * x2 * x3; + + std::vector<Coord> result = solve_cubic(a, b, c, d); + std::vector<Coord> x(3); x[0] = x1; x[1] = x2; x[2] = x3; + std::sort(x.begin(), x.end()); + + ASSERT_EQ(result.size(), 3u); + EXPECT_FLOAT_EQ(result[0], x[0]); + EXPECT_FLOAT_EQ(result[1], x[1]); + EXPECT_FLOAT_EQ(result[2], x[2]); + } + + // corner cases + // (x^2 + 7)(x - 2) + std::vector<Coord> r1 = solve_cubic(1, -2, 7, -14); + EXPECT_EQ(r1.size(), 1u); + EXPECT_FLOAT_EQ(r1[0], 2); + + // (x + 1)^2 (x-2) + std::vector<Coord> r2 = solve_cubic(1, 0, -3, -2); + ASSERT_EQ(r2.size(), 3u); + EXPECT_FLOAT_EQ(r2[0], -1); + EXPECT_FLOAT_EQ(r2[1], -1); + EXPECT_FLOAT_EQ(r2[2], 2); +} diff --git a/tests/rect-test.cpp b/tests/rect-test.cpp new file mode 100644 index 0000000..93733e5 --- /dev/null +++ b/tests/rect-test.cpp @@ -0,0 +1,368 @@ +/** @file + * @brief Unit tests for Rect, OptRect, IntRect, and OptIntRect. + * Uses the Google Testing Framework + *//* + * Authors: + * Krzysztof Kosiński <tweenk.pl@gmail.com> + * + * Copyright 2010 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/coord.h> +#include <2geom/rect.h> + +namespace Geom { + +typedef ::testing::Types<Coord, IntCoord> CoordTypes; + +TEST(RectTest, Upconversion) { + IntRect ir(0, -27, 10, 202); + Rect r_a(ir); + Rect r_b = ir; + OptIntRect oir_a(ir); + OptIntRect oir_b = ir; + OptRect or_a(oir_a); + OptRect or_b = oir_b; + + EXPECT_EQ(r_a, ir); + EXPECT_EQ(r_a, r_b); + EXPECT_EQ(r_a, *oir_a); + EXPECT_EQ(r_a, *oir_b); + EXPECT_EQ(r_a, *or_a); + EXPECT_EQ(r_a, *or_b); + EXPECT_EQ(oir_a, oir_b); + EXPECT_EQ(or_a, or_b); +} + +TEST(RectTest, Rounding) { + Rect r(-0.5, -0.5, 5.5, 5.5); + Rect r_small(0.3, 0.0, 0.6, 10.0); + Rect r_int(0,0,10,10); + IntRect out(-1, -1, 6, 6); + IntRect out_small(0, 0, 1, 10); + IntRect out_int(0,0,10,10); + OptIntRect in = IntRect(0, 0, 5, 5); + EXPECT_EQ(r.roundOutwards(), out); + EXPECT_EQ(r_small.roundOutwards(), out_small); + EXPECT_EQ(r_int.roundOutwards(), out_int); + EXPECT_EQ(r.roundInwards(), in); + EXPECT_EQ(r_small.roundInwards(), OptIntRect()); +} + +template <typename C> +class GenericRectTest : public ::testing::Test { +public: + typedef typename CoordTraits<C>::PointType CPoint; + typedef typename CoordTraits<C>::RectType CRect; + typedef typename CoordTraits<C>::OptRectType OptCRect; + CRect a, a2, b, c, d; + CRect int_ab, int_bc, uni_ab, uni_bc; + GenericRectTest() + : a(0, 0, 10, 10) + , a2(0, 0, 10, 10) + , b(-5, -5, 5, 5) + , c(-10, -10, -1, -1) + , d(1, 1, 9, 9) + , int_ab(0, 0, 5, 5) + , int_bc(-5, -5, -1, -1) + , uni_ab(-5, -5, 10, 10) + , uni_bc(-10, -10, 5, 5) + {} +}; + +TYPED_TEST_CASE(GenericRectTest, CoordTypes); + +TYPED_TEST(GenericRectTest, EqualityTest) { + typename TestFixture::CRect a(0, 0, 10, 10), a2(a), b(-5, -5, 5, 5); + typename TestFixture::OptCRect empty, oa = a; + + EXPECT_TRUE (a == a); + EXPECT_FALSE(a != a); + EXPECT_TRUE (a == a2); + EXPECT_FALSE(a != a2); + EXPECT_TRUE (empty == empty); + EXPECT_FALSE(empty != empty); + EXPECT_FALSE(a == empty); + EXPECT_TRUE (a != empty); + EXPECT_FALSE(empty == a); + EXPECT_TRUE (empty != a); + EXPECT_FALSE(a == b); + EXPECT_TRUE (a != b); + EXPECT_TRUE (a == oa); + EXPECT_FALSE(a != oa); +} + +TYPED_TEST(GenericRectTest, Intersects) { + typename TestFixture::CRect a(0, 0, 10, 10), b(-5, -5, 5, 5), c(-10, -10, -1, -1), d(1, 1, 9, 9); + typename TestFixture::OptCRect empty, oa(a), oc(c), od(d); + EXPECT_TRUE(a.intersects(a)); + EXPECT_TRUE(a.intersects(b)); + EXPECT_TRUE(b.intersects(a)); + EXPECT_TRUE(b.intersects(c)); + EXPECT_TRUE(c.intersects(b)); + EXPECT_TRUE(a.intersects(d)); + EXPECT_TRUE(d.intersects(a)); + EXPECT_FALSE(a.intersects(c)); + EXPECT_FALSE(c.intersects(a)); + EXPECT_FALSE(c.intersects(d)); + EXPECT_FALSE(empty.intersects(empty)); + EXPECT_FALSE(empty.intersects(oa)); + EXPECT_FALSE(oa.intersects(empty)); + EXPECT_TRUE(oa.intersects(od)); + EXPECT_FALSE(oa.intersects(oc)); +} + +/** + JonCruz failure: (10, 20)-(55,30) and (45,20)-(100,30) should intersect. +*/ + +TYPED_TEST(GenericRectTest, JonCruzRect) { + typename TestFixture::CRect a(10, 20, 55, 30), b(45, 20, 100,30); + typename TestFixture::OptCRect empty, oa(a), ob(b); + EXPECT_TRUE(a.intersects(a)); + EXPECT_TRUE(a.intersects(b)); + EXPECT_TRUE(b.intersects(a)); + EXPECT_TRUE(oa.intersects(oa)); + EXPECT_TRUE(oa.intersects(ob)); + EXPECT_TRUE(ob.intersects(oa)); +} + +TYPED_TEST(GenericRectTest, Intersection) { + typename TestFixture::CRect a(0, 0, 10, 10), b(-5, -5, 5, 5), c(-10, -10, -1, -1), d(1, 1, 9, 9); + typename TestFixture::CRect int_ab(0, 0, 5, 5), int_bc(-5, -5, -1, -1); + typename TestFixture::OptCRect empty, oa(a), ob(b); + + EXPECT_EQ(a & a, a); + EXPECT_EQ(a & b, int_ab); + EXPECT_EQ(b & c, int_bc); + EXPECT_EQ(intersect(b, c), int_bc); + EXPECT_EQ(intersect(a, a), a); + EXPECT_EQ(a & c, empty); + EXPECT_EQ(a & d, d); + EXPECT_EQ(a & empty, empty); + EXPECT_EQ(empty & empty, empty); + + oa &= ob; + EXPECT_EQ(oa, int_ab); + oa = a; + oa &= b; + EXPECT_EQ(oa, int_ab); + oa = a; + oa &= empty; + EXPECT_EQ(oa, empty); +} + +TYPED_TEST(GenericRectTest, Contains) { + typename TestFixture::CRect a(0, 0, 10, 10), b(-5, -5, 5, 5), c(-10, -10, -1, -1), d(1, 1, 9, 9); + typename TestFixture::CRect int_ab(0, 0, 5, 5), int_bc(-5, -5, -1, -1); + typename TestFixture::OptCRect empty, oa(a), od(d); + EXPECT_TRUE(a.contains(a)); + EXPECT_FALSE(a.contains(b)); + EXPECT_FALSE(b.contains(a)); + EXPECT_FALSE(a.contains(c)); + EXPECT_FALSE(c.contains(a)); + EXPECT_TRUE(a.contains(d)); + EXPECT_FALSE(d.contains(a)); + EXPECT_TRUE(a.contains(int_ab)); + EXPECT_TRUE(b.contains(int_ab)); + EXPECT_TRUE(b.contains(int_bc)); + EXPECT_TRUE(c.contains(int_bc)); + EXPECT_FALSE(int_ab.contains(a)); + EXPECT_FALSE(int_ab.contains(b)); + EXPECT_FALSE(int_bc.contains(b)); + EXPECT_FALSE(int_bc.contains(c)); + EXPECT_FALSE(empty.contains(empty)); + EXPECT_FALSE(empty.contains(od)); + EXPECT_TRUE(oa.contains(empty)); + EXPECT_TRUE(oa.contains(od)); + EXPECT_FALSE(od.contains(oa)); +} + +TYPED_TEST(GenericRectTest, Union) { + typename TestFixture::CRect a(0, 0, 10, 10), old_a(a), b(-5, -5, 5, 5), c(-10, -10, -1, -1), d(1, 1, 9, 9); + typename TestFixture::CRect int_ab(0, 0, 5, 5), int_bc(-5, -5, -1, -1); + typename TestFixture::CRect uni_ab(-5, -5, 10, 10), uni_bc(-10, -10, 5, 5); + typename TestFixture::OptCRect empty, oa(a), ob(b); + EXPECT_EQ(a | b, uni_ab); + EXPECT_EQ(b | c, uni_bc); + EXPECT_EQ(a | a, a); + EXPECT_EQ(a | d, a); + EXPECT_EQ(a | int_ab, a); + EXPECT_EQ(b | int_ab, b); + EXPECT_EQ(uni_ab | a, uni_ab); + EXPECT_EQ(uni_bc | c, uni_bc); + EXPECT_EQ(a | empty, a); + EXPECT_EQ(empty | empty, empty); + + a |= b; + EXPECT_EQ(a, uni_ab); + a = old_a; + a |= ob; + EXPECT_EQ(a, uni_ab); + a = old_a; + a |= empty; + EXPECT_EQ(a, old_a); + oa |= ob; + EXPECT_EQ(oa, uni_ab); + oa = old_a; + oa |= b; + EXPECT_EQ(oa, uni_ab); +} + +TYPED_TEST(GenericRectTest, Area) { + typename TestFixture::CRect a(0, 0, 10, 10), b(-5, -5, 5, 5), c(-10, -10, -1, -1), d(1, 1, 9, 9); + typename TestFixture::CRect zero(0,0,0,0); + EXPECT_EQ(a.area(), 100); + EXPECT_EQ(a.area(), a.width() * a.height()); + EXPECT_EQ(b.area(), 100); + EXPECT_EQ(c.area(), 81); + EXPECT_EQ(d.area(), 64); + EXPECT_FALSE(a.hasZeroArea()); + EXPECT_TRUE(zero.hasZeroArea()); +} + +TYPED_TEST(GenericRectTest, Emptiness) { + typename TestFixture::OptCRect empty, oa(0, 0, 10, 10); + EXPECT_TRUE(empty.empty()); + EXPECT_FALSE(empty); + EXPECT_TRUE(!empty); + EXPECT_FALSE(oa.empty()); + EXPECT_TRUE(oa); + EXPECT_FALSE(!oa); +} + +TYPED_TEST(GenericRectTest, Dimensions) { + typedef typename TestFixture::CPoint CPoint; + typename TestFixture::CRect a(-10, -20, 10, 20), b(-15, 30, 45, 90); + EXPECT_EQ(a.width(), 20); + EXPECT_EQ(a.height(), 40); + EXPECT_EQ(a.left(), -10); + EXPECT_EQ(a.top(), -20); + EXPECT_EQ(a.right(), 10); + EXPECT_EQ(a.bottom(), 20); + EXPECT_EQ(a.min(), CPoint(-10, -20)); + EXPECT_EQ(a.max(), CPoint(10, 20)); + EXPECT_EQ(a.minExtent(), a.width()); + EXPECT_EQ(a.maxExtent(), a.height()); + EXPECT_EQ(a.dimensions(), CPoint(20, 40)); + EXPECT_EQ(a.midpoint(), CPoint(0, 0)); + + EXPECT_EQ(b.width(), 60); + EXPECT_EQ(b.height(), 60); + EXPECT_EQ(b.left(), -15); + EXPECT_EQ(b.top(), 30); + EXPECT_EQ(b.right(), 45); + EXPECT_EQ(b.bottom(), 90); + EXPECT_EQ(b.min(), CPoint(-15, 30)); + EXPECT_EQ(b.max(), CPoint(45, 90)); + EXPECT_EQ(b.minExtent(), b.maxExtent()); + EXPECT_EQ(b.dimensions(), CPoint(60, 60)); + EXPECT_EQ(b.midpoint(), CPoint(15, 60)); +} + +TYPED_TEST(GenericRectTest, Modification) { + typedef typename TestFixture::CRect CRect; + typedef typename TestFixture::OptCRect OptCRect; + typedef typename TestFixture::CPoint CPoint; + CRect a(-1, -1, 1, 1); + a.expandBy(9); + EXPECT_EQ(a, CRect(-10, -10, 10, 10)); + a.setMin(CPoint(0, 0)); + EXPECT_EQ(a, CRect(0, 0, 10, 10)); + a.setMax(CPoint(20, 30)); + EXPECT_EQ(a, CRect(0, 0, 20, 30)); + a.setMax(CPoint(-5, -5)); + EXPECT_EQ(a, CRect(-5, -5, -5, -5)); + a.expandTo(CPoint(5, 5)); + EXPECT_EQ(a, CRect(-5, -5, 5, 5)); + a.expandTo(CPoint(0, 0)); + EXPECT_EQ(a, CRect(-5, -5, 5, 5)); + a.expandTo(CPoint(0, 15)); + EXPECT_EQ(a, CRect(-5, -5, 5, 15)); + a.expandBy(-10); + EXPECT_EQ(a, CRect(0, 5, 0, 5)); + EXPECT_EQ(a.midpoint(), CPoint(0, 5)); + a.unionWith(CRect(-20, 0, -10, 20)); + EXPECT_EQ(a, CRect(-20, 0, 0, 20)); + OptCRect oa(a); + oa.intersectWith(CRect(-10, -5, 5, 15)); + EXPECT_EQ(oa, OptCRect(-10, 0, 0, 15)); +} + +TYPED_TEST(GenericRectTest, OptRectDereference) { + typename TestFixture::CRect a(0, 0, 5, 5); + typename TestFixture::OptCRect oa(0, 0, 10, 10); + EXPECT_NE(a, oa); + a = *oa; + EXPECT_EQ(a, oa); +} + +TYPED_TEST(GenericRectTest, Offset) { + typename TestFixture::CRect a(0, 0, 5, 5), old_a(a), app1(-5, 0, 0, 5), amp1(5, 0, 10, 5), + app2(5, -10, 10, -5), amp2(-5, 10, 0, 15); + typename TestFixture::CPoint p1(-5, 0), p2(5, -10); + EXPECT_EQ(a + p1, app1); + EXPECT_EQ(a + p2, app2); + EXPECT_EQ(a - p1, amp1); + EXPECT_EQ(a - p2, amp2); + + a += p1; + EXPECT_EQ(a, app1); + a = old_a; + a += p2; + EXPECT_EQ(a, app2); + a = old_a; + a -= p1; + EXPECT_EQ(a, amp1); + a = old_a; + a -= p2; + EXPECT_EQ(a, amp2); +} + +TYPED_TEST(GenericRectTest, NearestEdgePoint) { + typename TestFixture::CRect a(0, 0, 10, 10); + typename TestFixture::CPoint p1(-5, 5), p2(15, 17), p3(6, 5), p4(3, 9); + typename TestFixture::CPoint r1(0, 5), r2(10, 10), r3(10, 5), r4(3, 10); + + EXPECT_EQ(a.nearestEdgePoint(p1), r1); + EXPECT_EQ(a.nearestEdgePoint(p2), r2); + EXPECT_EQ(a.nearestEdgePoint(p3), r3); + EXPECT_EQ(a.nearestEdgePoint(p4), r4); +} + +} // end 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 : diff --git a/tests/root-find-test.cpp b/tests/root-find-test.cpp new file mode 100644 index 0000000..b866f66 --- /dev/null +++ b/tests/root-find-test.cpp @@ -0,0 +1,156 @@ +#include <2geom/polynomial.h> +#include <vector> +#include <iterator> + +#include <2geom/sbasis.h> +#include <2geom/sbasis-poly.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/solver.h> +#include <time.h> + +using namespace std; +using namespace Geom; + +Poly lin_poly(double a, double b) { // ax + b + Poly p; + p.push_back(b); + p.push_back(a); + return p; +} + +Linear linear(double ax, double b) { + return Linear(b, ax+b); +} + +double uniform() { + return double(rand()) / RAND_MAX; +} + +int main() { + Poly a, b, r; + double timer_precision = 0.01; + double units = 1e6; // us + + a = Poly::linear(1, -0.3)*Poly::linear(1, -0.25)*Poly::linear(1, -0.2); + + std::cout << a <<std::endl; + SBasis B = poly_to_sbasis(a); + std::cout << B << std::endl; + Bezier bez; + sbasis_to_bezier(bez, B); + cout << bez << endl; + //copy(bez.begin(), bez.end(), ostream_iterator<double>(cout, ", ")); + cout << endl; + cout << endl; + cout << endl; + + std::vector<std::vector<double> > trials; + + // evenly spaced roots + for(int N = 2; N <= 5; N++) + { + std::vector<double> r; + for(int i = 0; i < N; i++) + r.push_back(double(i)/(N-1)); + trials.push_back(r); + } + // sort of evenish + for(int N = 0; N <= 5; N++) + { + std::vector<double> r; + for(int i = 0; i < N; i++) + r.push_back(double(i+0.5)/(2*N)); + trials.push_back(r); + } + // one at 0.1 + for(int N = 0; N <= 5; N++) + { + std::vector<double> r; + for(int i = 0; i < N; i++) + r.push_back(i+0.1); + trials.push_back(r); + } + for(int N = 0; N <= 6; N++) + { + std::vector<double> r; + for(int i = 0; i < N; i++) + r.push_back(i*0.8+0.1); + trials.push_back(r); + } + for(int N = 0; N <= 20; N++) + { + std::vector<double> r; + for(int i = 0; i < N/2; i++) { + r.push_back(0.1); + r.push_back(0.9); + } + trials.push_back(r); + } + for(int i = 0; i <= 20; i++) + { + std::vector<double> r; + for(int i = 0; i < 4; i++) { + r.push_back(uniform()*5 - 2.5); + r.push_back(0.9); + } + trials.push_back(r); + } + double ave_left = 0; + cout << "err from exact\n"; + for(auto & trial : trials) { + SBasis B = Linear(1.,1); + sort(trial.begin(), trial.end()); + for(double j : trial) { + B = B*linear(1, -j); + } + double left_time; + clock_t end_t = clock()+clock_t(timer_precision*CLOCKS_PER_SEC); + unsigned iterations = 0; + while(end_t > clock()) { + roots(B); + iterations++; + } + left_time = timer_precision*units/iterations; + vector<double> rt = roots(B); + double err = 0; + for(double r : rt) { + double best = fabs(r - trial[0]); + for(unsigned j = 1; j < trial.size(); j++) { + if(fabs(r - trial[j]) < best) + best = fabs(r - trial[j]); + } + err += best; + } + if(err > 1e-8){ + for(double j : trial) { + cout << j << ", "; + } + cout << endl; + } + cout << " e: " << err << std::endl; + ave_left += left_time; + } + cout << "average time = " << ave_left/trials.size() << std::endl; + + for(int i = 10; i >= 0; i--) { + vector<double> rt = roots(Linear(i,-1)); + for(double j : rt) { + cout << j << ", "; + } + cout << endl; + } + + return 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 : diff --git a/tests/rtree-performance-test.cpp b/tests/rtree-performance-test.cpp new file mode 100644 index 0000000..cf4bcd7 --- /dev/null +++ b/tests/rtree-performance-test.cpp @@ -0,0 +1,361 @@ +/* + * Copyright 2010 Evangelos Katsikaros <vkatsikaros at yahoo dot gr> + * + * 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/toys/toy-framework-2.h> + +#include <sstream> +#include <getopt.h> + +#include <SpatialIndex.h> +#include <glib.h> +//#include <glib/gtypes.h> + +using namespace Geom; + +// cmd argument stuff +char* arg_area_limit = NULL; +bool arg_area_limit_set = false; +bool arg_debug = false; + +int limit = 0; + +// spatial index ID management +SpatialIndex::id_type indexID; + +// list of rectangles +GList *items = NULL; + +// tree of rectangles +SpatialIndex::ISpatialIndex *tree; + +SpatialIndex::id_type test_indexID; + +void add_rectangle( int x, int y ); + +/* Simple Visitor used to search the tree. When Data is encountered + * we are supposed to call the render function of the Data + * */ +class SearchVisitor : public SpatialIndex::IVisitor { +public: + + void visitNode(const SpatialIndex::INode& n){ + } + + void visitData(const SpatialIndex::IData& d){ + /* this prototype: do nothing + * otherwise, render on buffer + * */ + } + + void visitData(std::vector<const SpatialIndex::IData*>& v) { + } +}; + + +/* we use the this visitor after each insertion in the tree + * The purpose is to validate that everything was stored properly + * and test the GList pointer storage. It has no other functional + * purpose. + * */ +class TestSearchVisitor : public SpatialIndex::IVisitor { +public: + + void visitNode(const SpatialIndex::INode& n) { + } + + void visitData(const SpatialIndex::IData& d){ + if( test_indexID == d.getIdentifier() ){ + byte* pData = 0; + uint32_t cLen = sizeof(GList*); + d.getData(cLen, &pData); + //do something... + GList* gl = reinterpret_cast<GList*>(pData); + Geom::Rect *member_data = (Geom::Rect *)gl->data; + double lala = member_data->bottom(); + std::cout << " Tree: " << lala << std::endl; + + delete[] pData; + } + } + + void visitData(std::vector<const SpatialIndex::IData*>& v) { + } +}; + + +int main(int argc, char **argv) { + + int c; + + //-------------------------------------------------------------------------- + // read cmd options + while (1) { + static struct option long_options[] = + { + /* These options set a flag. */ + /* These options don't set a flag. + We distinguish them by their indices. */ + {"area-limit", required_argument, 0, 'l'}, + {"help", no_argument, 0, 'h'}, + {"debug", no_argument, 0, 'd'}, + {0, 0, 0, 0} + }; + /* getopt_long stores the option index here. */ + int option_index = 0; + + c = getopt_long (argc, argv, "l:h:d", + long_options, &option_index); + + /* Detect the end of the options. */ + if (c == -1){ + break; + } + + switch (c) + { + case 'l': + arg_area_limit = optarg; + arg_area_limit_set = true; + break; + case 'h': + std::cerr << "Usage: " << argv[0] << " options\n" << std::endl ; + std::cerr << + " -l --area-limit=NUMBER minimum number in node.\n" << + " -d --debug Enable debug info (list/tree related).\n" << + " -h --help Print this help.\n" << std::endl; + exit(1); + break; + case 'd': + arg_debug = true; + break; + case '?': + /* getopt_long already printed an error message. */ + break; + + default: + abort (); + } + } + + // use some of the cmd options + if( arg_area_limit_set ) { + std::stringstream s1( arg_area_limit ); + s1 >> limit; + } + else { + limit = 100; + } + // end cmd options + //-------------------------------------------------------------------------- + + + double plow[2], phigh[2]; + // spatial index memory storage manager + SpatialIndex::IStorageManager *mem_mngr; + // initialize spatial indexing stuff + mem_mngr = SpatialIndex::StorageManager::createNewMemoryStorageManager(); + // fillFactor, indexCapacity, leafCapacity, dimensionality=2, variant=R*, indexIdentifier + tree = SpatialIndex::RTree::createNewRTree(*mem_mngr, 0.7, 25, 25, 2, SpatialIndex::RTree::RV_RSTAR, indexID); + + //------------------------------------------- + /* generate items. add_rectangle() stores them in both list and tree + * add rect every (20, 20). + * In area ((0,0), (1000, 1000)) add every (100,100) + * */ + for( int x_coord = -limit; x_coord <= limit; x_coord += 20 ) { + for( int y_coord = -limit; y_coord <= limit; y_coord += 20 ) { + if( x_coord >= 0 && x_coord <= 1000 && + y_coord >= 0 && y_coord <= 1000 ) + { + if( x_coord % 100 == 0 && y_coord % 100 == 0) { + add_rectangle( x_coord, y_coord ); + } + else{ + add_rectangle( x_coord, y_coord ); + } + } + else{ + add_rectangle( x_coord, y_coord ); + } + + } + } + std::cout << "Area of objects: ( -" << limit + << ", -" << limit + << " ), ( " << limit + << ", " << limit + << " )" << std::endl; + std::cout << "Number of Objects (indexID): " << indexID << std::endl; + // std::cout << "GListElements: " << g_list_length << std::endl; + + //------------------------------------------- + // Traverse list + Geom::Point sa_start = Point( 0, 0 ); + Geom::Point sa_end = Point( 1000, 1000 ); + Geom::Rect search_area = Rect( sa_start, sa_end ); + + Timer list_timer; + list_timer.ask_for_timeslice(); + list_timer.start(); + + for (GList *list = items; list; list = list->next) { + Geom::Rect *child = (Geom::Rect *)list->data; + if ( search_area.intersects( *child ) ) + { + /* this prototype: do nothing + * otherwise, render on buffer + * */ + } + } + Timer::Time the_list_time = list_timer.lap(); + + std::cout << std::endl; + std::cout << "GList (full scan): " << the_list_time << std::endl; + + //------------------------------------------- + // Search tree - good case + Timer tree_timer; + tree_timer.ask_for_timeslice(); + tree_timer.start(); + + /* We search only the (0,0), (1000, 1000) where the items are less dense. + * We expect a good performance versus the list + * */ + // TODO IMPORTANT !!! check the plow, phigh + // plow[0] = x1; plow[1] = y1; + // phigh[0] = x2; phigh[1] = y2; + + plow[0] = 0; + plow[1] = 0; + phigh[0] = 1000; + phigh[1] = 1000; + + SpatialIndex::Region search_region = SpatialIndex::Region(plow, phigh, 2); + SearchVisitor vis = SearchVisitor(); + tree->intersectsWithQuery( search_region, vis ); + + Timer::Time the_tree_time = tree_timer.lap(); + std::cout << "Rtree (good): " << the_tree_time << std::endl; + + + //------------------------------------------- + // Search tree - worst case + Timer tree_timer_2; + tree_timer_2.ask_for_timeslice(); + tree_timer_2.start(); + + /* search the whole area, so all items are returned */ + plow[0] = -limit - 100; + plow[1] = -limit - 100; + phigh[0] = limit + 100; + phigh[1] = limit + 100; + + SpatialIndex::Region search_region_2 = SpatialIndex::Region(plow, phigh, 2); + SearchVisitor vis_2 = SearchVisitor(); + tree->intersectsWithQuery( search_region_2, vis_2 ); + + Timer::Time the_tree_time_2 = tree_timer_2.lap(); + std::cout << "Rtree (full scan): " << the_tree_time_2 << std::endl; + + return 0; +} + + + +/* Adds rectangles in a GList and a SpatialIndex rtree + * */ +void add_rectangle( int x, int y ) { + + Geom::Point starting_point = Point( x, y ); + Geom::Point ending_point = Point( x + 10, y + 10 ); + Geom::Rect rect_to_add = Rect( starting_point, ending_point ); + items = g_list_append( items, &rect_to_add ); + + if( arg_debug ) { + // fetch the last rect from the list + Geom::Rect *member_data = (Geom::Rect *)( g_list_last( items ) )->data; + double lala = member_data->bottom(); + std::cout << "List (" << indexID << "): " << lala; + } + + /* Create a SpatialIndex region + * plow = left-bottom corner + * phigh = top-right corner + * [0] = dimension X + * [1] = dimension Y + * */ + double plow[2], phigh[2]; + + plow[0] = rect_to_add.left() ; + plow[1] = rect_to_add.bottom(); + phigh[0] = rect_to_add.right(); + phigh[1] = rect_to_add.top(); + + SpatialIndex::Region r = SpatialIndex::Region(plow, phigh, 2); + /* Store Glist pointer size and GList pointer as the associated data + * In inkscape this can be used to directly call render hooked functions + * from SPCanvasItems + * */ + tree->insertData( sizeof(GList*), reinterpret_cast<const byte*>( g_list_last( items ) ), r, indexID); + + // tree->insertData(0, 0, r, indexID); + /* not used. Store zero size and a null pointer as the associated data. + * indexId is used to retrieve from a mapping each rect + * (example a hash map, or the indexID is also vector index) + * */ + + if( arg_debug ) { + test_indexID = indexID; + /* every time we add a rect, search all the tree to find the last + * inserted ID. This is not performance-wise good (rtree only good for + * spatial queries) this is just used for debugging reasons + * */ + plow[0] = -limit - 100; + plow[1] = -limit - 100; + phigh[0] = limit + 100; + phigh[1] = limit + 100; + + SpatialIndex::Region test_search_region = SpatialIndex::Region(plow, phigh, 2); + TestSearchVisitor test_vis = TestSearchVisitor(); + // search the tree for the region. Visitor implements the search function hooks + tree->intersectsWithQuery( test_search_region, test_vis ); + } + + indexID++; +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)(c-basic-offset . 4)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/tests/rtree-test.cpp b/tests/rtree-test.cpp new file mode 100644 index 0000000..f01007a --- /dev/null +++ b/tests/rtree-test.cpp @@ -0,0 +1,158 @@ +/* + * Copyright 2009 Evangelos Katsikaros <vkatsikaros at yahoo dot gr> + * + * 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. + */ + +/* + initial toy for redblack trees +*/ + + +#include <2geom/rtree.h> + +#include <time.h> +#include <vector> + +#include <sstream> +#include <getopt.h> + + + + +//using std::vector; +using namespace Geom; +using namespace std; + +sadfsdfasdfasdfa + +int main(int argc, char **argv) { + + long test_seed = 1243716824; + + char* min_arg = NULL; + char* max_arg = NULL; + char* filename_arg = NULL; + + int set_min_max = 0; + + int c; + + while (1) + { + static struct option long_options[] = + { + /* These options set a flag. */ + /* These options don't set a flag. + We distinguish them by their indices. */ + {"min-nodes", required_argument, 0, 'n'}, + {"max-nodes", required_argument, 0, 'm'}, + {"input-file", required_argument, 0, 'f'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0} + }; + /* getopt_long stores the option index here. */ + int option_index = 0; + + c = getopt_long (argc, argv, "n:m:f:h", + long_options, &option_index); + + /* Detect the end of the options. */ + if (c == -1){ + break; + } + + switch (c) + { + case 'n': + min_arg = optarg; + set_min_max += 1; + break; + + + case 'm': + max_arg = optarg; + set_min_max += 2; + break; + + case 'f': + filename_arg = optarg; + set_min_max += 3; + break; + + + case 'h': + std::cerr << "Usage: " << argv[0] << " options\n" << std::endl ; + std::cerr << + " -n --min-nodes=NUMBER minimum number in node.\n" << + " -m --max-nodes=NUMBER maximum number in node.\n" << + " -f --max-nodes=NUMBER maximum number in node.\n" << + " -h --help Print this help.\n" << std::endl; + exit(1); + break; + + + case '?': + /* getopt_long already printed an error message. */ + break; + + default: + abort (); + } + } + + unsigned rmin = 0; + unsigned rmax = 0; + + if( set_min_max == 6 ){ + stringstream s1( min_arg ); + s1 >> rmin; + + stringstream s2( max_arg ); + s2 >> rmax; + + + if( rmax <= rmin || rmax < 2 || rmin < 1 ){ + std::cerr << "Rtree set to 2, 3" << std::endl ; + rmin = 2; + rmax = 3; + } + } + else{ + std::cerr << "Rtree set to 2, 3 ." << std::endl ; + rmin = 2; + rmax = 3; + } + + + + std::cout << "rmin: " << rmin << " rmax:" << rmax << " filename_arg:" << filename_arg << std::endl; + + RTree rtree( rmin, rmax, QUADRATIC_SPIT ); + + srand(1243716824); + rand() % 10; + + return 0; +} diff --git a/tests/sbasis-test.cpp b/tests/sbasis-test.cpp new file mode 100644 index 0000000..045d409 --- /dev/null +++ b/tests/sbasis-test.cpp @@ -0,0 +1,268 @@ +#include "testing.h" +#include <iostream> + +#include <2geom/bezier.h> +#include <2geom/sbasis.h> +#include <2geom/sbasis-to-bezier.h> +#include <vector> +#include <iterator> +#include <glib.h> + +using namespace std; +using namespace Geom; + +bool are_equal(SBasis const &A, SBasis const &B) { + int maxSize = max(A.size(), B.size()); + double t = 0., dt = 1./maxSize; + + for(int i = 0; i <= maxSize; i++) { + EXPECT_FLOAT_EQ(A.valueAt(t), B.valueAt(t));// return false; + t += dt; + } + return true; +} + +class SBasisTest : public ::testing::Test { +protected: + friend class Geom::SBasis; + SBasisTest() + : zero(fragments[0]) + , unit(fragments[1]) + , hump(fragments[2]) + , wiggle(fragments[3]) + { + zero = SBasis(Bezier(0.0).toSBasis()); + unit = SBasis(Bezier(0.0,1.0).toSBasis()); + hump = SBasis(Bezier(0,1,0).toSBasis()); + wiggle = SBasis(Bezier(0,1,-2,3).toSBasis()); + } + + SBasis fragments[4]; + SBasis &zero, &unit, &hump, &wiggle; +}; + +TEST_F(SBasisTest, UnitTests) { + EXPECT_TRUE(Bezier(0,0,0,0).toSBasis().isZero()); + EXPECT_TRUE(Bezier(0,1,2,3).toSBasis().isFinite()); + + // note: "size" of sbasis equals half the number of coefficients + EXPECT_EQ(2u, Bezier(0,2,4,5).toSBasis().size()); + EXPECT_EQ(2u, hump.size()); +} + +TEST_F(SBasisTest, ValueAt) { + EXPECT_EQ(0.0, wiggle.at0()); + EXPECT_EQ(3.0, wiggle.at1()); + EXPECT_EQ(0.0, wiggle.valueAt(0.5)); + EXPECT_EQ(0.0, wiggle(0.5)); +} + +TEST_F(SBasisTest, MultiDerivative) { + vector<double> vnd = wiggle.valueAndDerivatives(0.5, 5); + expect_array((const double[]){0,0,12,72,0,0}, vnd); +} + /* +TEST_F(SBasisTest, DegreeElevation) { + EXPECT_TRUE(are_equal(wiggle, wiggle)); + SBasis Q = wiggle; + SBasis P = Q.elevate_degree(); + EXPECT_EQ(P.size(), Q.size()+1); + //EXPECT_EQ(0, P.forward_difference(1)[0]); + EXPECT_TRUE(are_equal(Q, P)); + Q = wiggle; + P = Q.elevate_to_degree(10); + EXPECT_EQ(10, P.order()); + EXPECT_TRUE(are_equal(Q, P)); + //EXPECT_EQ(0, P.forward_difference(10)[0]); +}*/ +//std::pair<SBasis, SBasis > subdivide(Coord t); + +SBasis linear_root(double t) { + return SBasis(Linear(0-t, 1-t)); +} + +SBasis array_roots(vector<double> x) { + SBasis b(1); + for(double i : x) { + b = multiply(b, linear_root(i)); + } + return b; +} + + /*TEST_F(SBasisTest, Deflate) { + SBasis b = array_roots(vector_from_array((const double[]){0,0.25,0.5})); + EXPECT_FLOAT_EQ(0, b.at0()); + b = b.deflate(); + EXPECT_FLOAT_EQ(0, b.valueAt(0.25)); + b = b.subdivide(0.25).second; + EXPECT_FLOAT_EQ(0, b.at0()); + b = b.deflate(); + const double rootposition = (0.5-0.25) / (1-0.25); + EXPECT_FLOAT_EQ(0, b.valueAt(rootposition)); + b = b.subdivide(rootposition).second; + EXPECT_FLOAT_EQ(0, b.at0()); +}*/ + +TEST_F(SBasisTest, Roots) { + expect_array((const double[]){0, 0.5, 0.5}, roots(wiggle)); + + // The results of our rootfinding are at the moment fairly inaccurate. + double eps = 5e-4; + + vector<vector<double> > tests; + tests.push_back(vector_from_array((const double[]){0})); + tests.push_back(vector_from_array((const double[]){0.5})); + tests.push_back(vector_from_array((const double[]){0.25,0.75})); + tests.push_back(vector_from_array((const double[]){0.5,0.5})); + tests.push_back(vector_from_array((const double[]){0, 0.2, 0.6,0.6, 1})); + tests.push_back(vector_from_array((const double[]){.1,.2,.3,.4,.5,.6})); + tests.push_back(vector_from_array((const double[]){0.25,0.25,0.25,0.75,0.75,0.75})); + + for(auto & test : tests) { + SBasis b = array_roots(test); + std::cout << test << ": " << b << std::endl; + std::cout << roots(b) << std::endl; + EXPECT_vector_near(test, roots(b), eps); + } + + vector<Linear> broken; + broken.emplace_back(0, 42350.1); + broken.emplace_back(-71082.3, -67071.5); + broken.emplace_back(1783.41, 796047); + SBasis b(broken); + Bezier bz; + sbasis_to_bezier(bz, b); + cout << "roots(SBasis(broken))\n"; + for(int i = 0; i < 10; i++) { + double t = i*0.01 + 0.1; + cout << b(t) << "," << bz(t) << endl; + } + cout << roots(b) << endl; + EXPECT_EQ(0, bz[0]); + //bz = bz.deflate(); + cout << bz << endl; + cout << bz.roots() << endl; +} + +TEST_F(SBasisTest, Subdivide) { + std::vector<std::pair<SBasis, double> > errors; + for (unsigned i = 0; i < 10000; ++i) { + double t = g_random_double_range(0, 1e-6); + for (auto & input : fragments) { + std::pair<SBasis, SBasis> result; + result.first = portion(input, 0, t); + result.second = portion(input, t, 1); + + // the endpoints must correspond exactly + EXPECT_EQ(result.first.at0(), input.at0()); + EXPECT_EQ(result.first.at1(), result.second.at0()); + EXPECT_EQ(result.second.at1(), input.at1()); + + // ditto for valueAt + EXPECT_EQ(result.first.valueAt(0), input.valueAt(0)); + EXPECT_EQ(result.first.valueAt(1), result.second.valueAt(0)); + EXPECT_EQ(result.second.valueAt(1), input.valueAt(1)); + + if (result.first.at1() != result.second.at0()) { + errors.emplace_back(input, t); + } + } + } + if (!errors.empty()) { + std::cout << "Found " << errors.size() << " subdivision errors" << std::endl; + for (unsigned i = 0; i < errors.size(); ++i) { + std::cout << "Error #" << i << ":\n" + << "SBasis: " << errors[i].first << "\n" + << "t: " << format_coord_nice(errors[i].second) << std::endl; + } + } +} + +TEST_F(SBasisTest, Reverse) { + SBasis reverse_wiggle = reverse(wiggle); + EXPECT_EQ(reverse_wiggle.at0(), wiggle.at1()); + EXPECT_EQ(reverse_wiggle.at1(), wiggle.at0()); + EXPECT_EQ(reverse_wiggle.valueAt(0.5), wiggle.valueAt(0.5)); + EXPECT_EQ(reverse_wiggle.valueAt(0.25), wiggle.valueAt(0.75)); + EXPECT_TRUE(are_equal(reverse(reverse_wiggle), wiggle)); +} + +TEST_F(SBasisTest,Operators) { + //cout << "scalar operators\n"; + //cout << hump + 3 << endl; + //cout << hump - 3 << endl; + //cout << hump*3 << endl; + //cout << hump/3 << endl; + + //cout << "SBasis derivative(const SBasis & a);\n"; + //std::cout << derivative(hump) <<std::endl; + //std::cout << integral(hump) <<std::endl; + + EXPECT_TRUE(are_equal(derivative(integral(wiggle)), wiggle)); + //std::cout << derivative(integral(hump)) << std::endl; + expect_array((const double []){0.5}, roots(derivative(hump))); + + EXPECT_TRUE(bounds_fast(hump)->contains(Interval(0,hump.valueAt(0.5)))); + + EXPECT_EQ(Interval(0,hump.valueAt(0.5)), *bounds_exact(hump)); + + Interval tight_local_bounds(min(hump.valueAt(0.3),hump.valueAt(0.6)), + hump.valueAt(0.5)); + EXPECT_TRUE(bounds_local(hump, Interval(0.3, 0.6))->contains(tight_local_bounds)); + + SBasis Bs[] = {unit, hump, wiggle}; + for(auto B : Bs) { + SBasis product = multiply(B, B); + for(int i = 0; i <= 16; i++) { + double t = i/16.0; + double b = B.valueAt(t); + EXPECT_FLOAT_EQ(b*b, product.valueAt(t)); + } + } +} + +TEST_F(SBasisTest, ToCubicBezier) +{ + vector<double> params = { 0, 1, -2, 3 }; + + D2<SBasis> sb(wiggle, wiggle); + vector<Point> bz; + sbasis_to_cubic_bezier(bz, sb); + for (int i = 0; i < params.size(); i++) { + EXPECT_FLOAT_EQ(bz[i][0], params[i]); + EXPECT_FLOAT_EQ(bz[i][1], params[i]); + } +} + +TEST_F(SBasisTest, Roundtrip) +{ + auto bz1 = Bezier(1, -2, 3, 7, 11, -24, 42, -1, 9, 1); + auto sbasis = bz1.toSBasis(); + Bezier bz2; + sbasis_to_bezier(bz2, sbasis); + ASSERT_EQ(bz1, bz2); + + std::vector<Point> pts; + for (int i = 0; i < bz1.size(); i++) { + pts.emplace_back(bz1[i], bz1[i]); + } + D2<SBasis> sbasis_d2; + bezier_to_sbasis(sbasis_d2, pts); + ASSERT_EQ(sbasis_d2[X], sbasis); + ASSERT_EQ(sbasis_d2[Y], sbasis); + D2<Bezier> bz2_d2; + sbasis_to_bezier(bz2_d2, sbasis_d2); + ASSERT_EQ(bz2_d2[X], bz1); + ASSERT_EQ(bz2_d2[Y], bz1); +} + +/* + 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 : diff --git a/tests/sbasis-text-test.cpp b/tests/sbasis-text-test.cpp new file mode 100644 index 0000000..ef96407 --- /dev/null +++ b/tests/sbasis-text-test.cpp @@ -0,0 +1,225 @@ +#include <iostream> +#include <math.h> +#include <cassert> +#include <2geom/sbasis.h> +#include <2geom/sbasis-poly.h> +#include <iterator> +#include <2geom/point.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/solver.h> + +using namespace Geom; + +Poly roots_to_poly(double *a, unsigned n) { + Poly r; + r.push_back(1); + + for(unsigned i = 0; i < n; i++) { + Poly p; + p.push_back(-a[i]); + p.push_back(1); + r = r*p; + } + return r; +} + +unsigned small_rand() { + return (rand() & 0xff) + 1; +} + +double uniform() { + return double(rand()) / RAND_MAX; +} + +int main() { + SBasis P0(Linear(0.5, -1)), P1(Linear(3, 1)); + Linear one(1,1); + + std::cout << "round tripping of poly conversions\n"; + std::cout << P0 + << "=>" << sbasis_to_poly(P0) + << "=>" << poly_to_sbasis(sbasis_to_poly(P0)) + << std::endl; + + std::cout << "derivatives and integrals\n"; + + Poly test; + for(int i = 0; i < 4; i++) + test.push_back(1); + + SBasis test_sb = poly_to_sbasis(test); + std::cout << test << "(" << test.size() << ")" + << " == " + << test_sb << "(" << test_sb.size() << ")" + << std::endl; + std::cout << "derivative\n"; + std::cout << derivative(test) + << " == " + << sbasis_to_poly(derivative(test_sb)) + << std::endl; + + std::cout << "integral\n"; + std::cout << integral(test) + << " == " + << sbasis_to_poly(integral(test_sb)) + << std::endl; + + std::cout << "evaluation\n"; + std::cout << integral(test)(0.3) - integral(test)(0.) + << " == " + << integral(test_sb)(0.3) - integral(test_sb)(0.) + << std::endl; + + std::cout << "multiplication\n"; + std::cout << (test*test) + << "\n == \n" + << sbasis_to_poly(multiply(test_sb,test_sb)) + << std::endl; + std::cout << poly_to_sbasis(test*test) + << "\n == \n" + << multiply(test_sb,test_sb) + << std::endl; + + std::cout << "sqrt\n"; + std::cout << test + << "\n == \n" + << sbasis_to_poly(sqrt(multiply(test_sb,test_sb),10)) + << std::endl; + SBasis radicand = sqrt(test_sb,10); + std::cout << sbasis_to_poly(truncate(multiply(radicand, radicand),5)) + << "\n == \n" + << test + << std::endl; + + std::cout << "division\n"; + std::cout << test + << "\n == \n" + << sbasis_to_poly(divide(multiply(test_sb,test_sb),test_sb, 20)) + << std::endl; + std::cout << divide(test_sb, radicand,5) + << "\n == \n" + << truncate(radicand,6) + << std::endl; + + std::cout << "composition\n"; + std::cout << (compose(test,sbasis_to_poly(Linear(0.5,1)))) + << "\n == \n" + << sbasis_to_poly(compose(test_sb,Linear(0.5,1))) + << std::endl; + std::cout << poly_to_sbasis(compose(test,test)) + << "\n == \n" + << compose(test_sb,test_sb) + << std::endl + << std::endl; + std::cout << (compose(sbasis_to_poly(Linear(1,2)),sbasis_to_poly(Linear(-1,0)))) + << std::endl; + std::cout << (compose(SBasis(Linear(1,2)),SBasis(Linear(-1,0)))) + << std::endl; + + std::cout << "inverse of x - 1\n"; + std::cout << sbasis_to_poly(inverse(Linear(-1,0),2)) + << " == y + 1\n"; + std::cout << "f^-1(f(x)) = " + << sbasis_to_poly(compose(inverse(Linear(-1,0),2), + Linear(-1,0))) + << std::endl + << std::endl; + + std::cout << "inverse of 3x - 2\n"; + std::cout << sbasis_to_poly(inverse(Linear(-2,1),2)) + << " == (y + 2)/3\n"; + std::cout << "f^-1(f(x)) = " + << sbasis_to_poly(compose(inverse(Linear(-2,1),2), + Linear(-2,1))) + << std::endl + << std::endl; + + + std::cout << "inverse of sqrt(" << sbasis_to_poly(Linear(1,4)) << ") - 1\n"; + SBasis A = sqrt(Linear(1,4), 5) - one; + Poly P; + P.push_back(0); + P.push_back(2./3); + P.push_back(1./3); + + std::cout << "2 term approximation\n"; + std::cout << sbasis_to_poly(inverse(A,2)) + << "\n == \n" + << P + << std::endl; + std::cout << "general approximation\n"; + std::cout << sbasis_to_poly(inverse(A,5)) + << "\n == \n" + << P + << std::endl; + + { + std::cout << "inverse of (x^2+2x)/3\n"; + SBasis A = poly_to_sbasis(P); + SBasis I = inverse(A,10); + std::cout << sbasis_to_poly(truncate(compose(A, I), 10)) + << " == x\n" + << std::endl; + std::cout << sbasis_to_poly(truncate(compose(I, A), 10)) + << " == x\n" + << std::endl; + std::cout << sbasis_to_poly(truncate(I - (sqrt(Linear(1,4), 10) - one), 10)) + << std::endl; + } +#ifdef HAVE_GSL + for(int i = 0 ; i < 10; i++) { + Poly P; + P.push_back(0); + P.push_back(1); + for(int j = 0 ; j < 2; j++) { + P.push_back((uniform()-0.5)/10); + } + std::vector<std::complex<double> > prod_root = solve(derivative(P)); + copy(prod_root.begin(), prod_root.end(), + std::ostream_iterator<std::complex<double> >(std::cout, ",\t")); + std::cout << std::endl; + std::cout << "inverse of (" << P << " )\n"; + for(int k = 1; k < 30; k++) { + SBasis A = poly_to_sbasis(P); + SBasis I = inverse(A,k); + SBasis err = compose(A, I) - Linear(0,1); // ideally is 0 + std::cout << truncate(err, k).tailError(0) + << std::endl; + /*std::cout << sbasis_to_poly(err) + << " == x\n" + << std::endl;*/ + } + } +#endif + /*double roots[] = {0.1,0.2,0.6}; + Poly prod = roots_to_poly(roots, sizeof(roots)/sizeof(double)); + std::cout << "real solve\n"; + std::cout << prod + << " solves as "; + std::vector<std::complex<double> > prod_root = solve(prod); + copy(prod_root.begin(), prod_root.end(), + std::ostream_iterator<std::complex<double> >(std::cout, ",\t")); + std::cout << std::endl; + + SBasis prod_sb = poly_to_sbasis(prod); + std::vector<double> bez = sbasis_to_bezier(prod_sb, prod_sb.size()); + + copy(bez.begin(), bez.end(), + std::ostream_iterator<double>(std::cout, ",\t")); + std::cout << std::endl;*/ + + /*std::cout << "crossing count = " + << crossing_count(bez, bez.size()) + << std::endl;*/ +} + +/* + 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 : 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 : diff --git a/tests/test_pwsb.py b/tests/test_pwsb.py new file mode 100644 index 0000000..6182d40 --- /dev/null +++ b/tests/test_pwsb.py @@ -0,0 +1,67 @@ +#!/usr/bin/python + +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "py2geom")) + +from py2geom import * +import py2geom +import numpy +import random +from py2geom_glue import * + +def poly_to_sbasis(p): + sb = SBasis() + s = numpy.poly1d([-1, 1, 0]) + while True: + q,r = p / s + x = Linear(r[0],r[1]+r[0]) + sb.append(x) + p = q + if len(list(p)) <= 1 and p[0] == 0: + return sb + +def sbasis_to_poly(sb): + p = numpy.poly1d([0]) + s = numpy.poly1d([-1, 1, 0]) + sp = numpy.poly1d([1]) + for sbt in sb: + p += sp*(sbt[0]*(numpy.poly1d([-1,1])) + sbt[1]*(numpy.poly1d([1,0]))) + sp *= s + return p + +random.seed(1) +trial = numpy.poly1d([random.randrange(0,10) for x in range(6)]) + +sb = poly_to_sbasis(trial) + +pwsb = PiecewiseSBasis() +pwsb.push_seg(sb) +pwsb.push_cut(0) +pwsb.push_cut(1) +print pwsb.size() +print "invariants:", pwsb.invariants() +print pwsb(0) + +def l2s(l): + sb = py2geom.SBasis() + sb.append(l) + return sb + +X = l2s(py2geom.Linear(0, 1)) +OmX = l2s(py2geom.Linear(1, 0)) +def bezier_to_sbasis(handles, order): + print "b2s:", handles, order + if(order == 0): + return l2s(py2geom.Linear(handles[0])) + elif(order == 1): + return l2s(py2geom.Linear(handles[0], handles[1])) + else: + return (py2geom.multiply(OmX, bezier_to_sbasis(handles[:-1], order-1)) + + py2geom.multiply(X, bezier_to_sbasis(handles[1:], order-1))) + + +for bz in [[0,1,0], [0,1,2,3]]: + sb = bezier_to_sbasis(bz, len(bz)-1) + print bz + print sb + print sbasis_to_bezier(sb,0) diff --git a/tests/test_py2geom.py b/tests/test_py2geom.py new file mode 100644 index 0000000..d6ec83e --- /dev/null +++ b/tests/test_py2geom.py @@ -0,0 +1,75 @@ +#!/usr/bin/python + +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "py2geom")) + +from py2geom import * +import py2geom + +P = Point(1,2) +Q = Point(3,5) +print P, Q, P+Q + +print L2(P) +print cross(P,Q) +#print dir(py2geom) + +import numpy + +ply = numpy.poly1d([1,0,2]) + +print ply(0.5) +t = numpy.poly1d([1,0]) +q,r = ply / t +print q,r + +print ply +print q*t + r +print ply + +def poly_to_sbasis(p): + sb = SBasis() + s = numpy.poly1d([-1, 1, 0]) + while True: + q,r = p / s + #print "r:", r + print "r:", repr(r) + x = Linear(r[0],r[1]+r[0]) + sb.append(x) + p = q + print "q:", repr(p), len(list(p)) + if len(list(p)) <= 1 and p[0] == 0: + return sb + +def sbasis_to_poly(sb): + p = numpy.poly1d([0]) + s = numpy.poly1d([-1, 1, 0]) + sp = numpy.poly1d([1]) + for sbt in sb: + p += sp*(sbt[0]*(numpy.poly1d([-1,1])) + sbt[1]*(numpy.poly1d([1,0]))) + sp *= s + return p + +trial = numpy.poly1d([1,0,2]) +sb = poly_to_sbasis(trial) +print repr(trial),"p2sb:", sb +print "and back again:", repr(sbasis_to_poly(sb)) +print repr(sbasis_to_poly(derivative(sb))), repr(trial.deriv()) + +print "unit tests:" +x = Linear(0,1) +sb = SBasis() +sb.append(x) +print sb +sb = sb*sb +print sb +print sb[0] + +print "terms" +for i in range(6): + sb = SBasis() + for j in range(3): + sb.append(Linear(i==2*j,i==2*j+1)) + print sb + + print sbasis_to_poly(sb) diff --git a/tests/testing.h b/tests/testing.h new file mode 100644 index 0000000..40a588d --- /dev/null +++ b/tests/testing.h @@ -0,0 +1,186 @@ +#include "gtest/gtest.h" +#include <vector> +#include <2geom/coord.h> +#include <2geom/interval.h> +#include <2geom/intersection.h> + +// streams out a vector +template <class T> +std::ostream& +operator<< (std::ostream &out, const std::vector<T, + std::allocator<T> > &v) +{ + typedef std::ostream_iterator<T, char, + std::char_traits<char> > Iter; + + std::copy (v.begin (), v.end (), Iter (out, " ")); + + return out; +} + +template <typename T, unsigned xn> +std::vector<T> vector_from_array(const T (&x)[xn]) { + std::vector<T> v; + for(unsigned i = 0; i < xn; i++) { + v.push_back(x[i]); + } + return v; +} + +template <typename T, unsigned xn> +void expect_array(const T (&x)[xn], std::vector<T> y) { + EXPECT_EQ(xn, y.size()); + for(unsigned i = 0; i < y.size(); i++) { + EXPECT_EQ(x[i], y[i]); + } +} + +Geom::Interval bound_vector(std::vector<double> const &v) { + double low = v[0]; + double high = v[0]; + for(double i : v) { + low = std::min(i, low); + high = std::max(i, high); + } + return Geom::Interval(low-1, high-1); +} + + +// Custom assertion formatting predicates + +template <typename T> +::testing::AssertionResult ObjectNear(char const *l_expr, + char const *r_expr, + char const */*eps_expr*/, + T const &l, + T const &r, + Geom::Coord eps) +{ + if (!Geom::are_near(l, r, eps)) { + return ::testing::AssertionFailure() << "Objects are not near\n" + << "First object: " << l_expr << "\n" + << "Value: " << l << "\n" + << "Second object: " << r_expr << "\n" + << "Value: " << r << "\n" + << "Threshold: " << Geom::format_coord_nice(eps) << std::endl; + } + return ::testing::AssertionSuccess(); +} + +template <typename T> +::testing::AssertionResult ObjectNotNear(char const *l_expr, + char const *r_expr, + char const */*eps_expr*/, + T const &l, + T const &r, + Geom::Coord eps) +{ + if (Geom::are_near(l, r, eps)) { + return ::testing::AssertionFailure() << "Objects are near\n" + << "First object: " << l_expr << "\n" + << "Value: " << l << "\n" + << "Second object: " << r_expr << "\n" + << "Value: " << r << "\n" + << "Threshold: " << Geom::format_coord_nice(eps) << std::endl; + } + return ::testing::AssertionSuccess(); +} + +#define EXPECT_near(a, b, eps) EXPECT_PRED_FORMAT3(ObjectNear, a, b, eps) +#define EXPECT_not_near(a, b, eps) EXPECT_PRED_FORMAT3(ObjectNotNear, a, b, eps) + + + +template <typename T> +::testing::AssertionResult VectorEqual(char const *l_expr, + char const *r_expr, + std::vector<T> const &l, + std::vector<T> const &r) +{ + if (l.size() != r.size()) { + return ::testing::AssertionFailure() << "Vectors differ in size\n" + << l_expr << " has size " << l.size() << "\n" + << r_expr << " has size " << r.size() << std::endl; + } + for (unsigned i = 0; i < l.size(); ++i) { + if (!(l[i] == r[i])) { + return ::testing::AssertionFailure() << "Vectors differ" + << "\nVector: " << l_expr + << "\nindex " << i << " contains: " << l[i] + << "\nVector:" << r_expr + << "\nindex " << i << " contains: " << r[i] << std::endl; + } + } + return ::testing::AssertionSuccess(); +} + +template <typename T> +::testing::AssertionResult VectorNear(char const *l_expr, + char const *r_expr, + char const */*eps_expr*/, + std::vector<T> const &l, + std::vector<T> const &r, + Geom::Coord eps) +{ + if (l.size() != r.size()) { + return ::testing::AssertionFailure() << "Vectors differ in size\n" + << l_expr << "has size " << l.size() << "\n" + << r_expr << "has size " << r.size() << std::endl; + } + for (unsigned i = 0; i < l.size(); ++i) { + if (!Geom::are_near(l[i], r[i], eps)) { + return ::testing::AssertionFailure() << "Vectors differ by more than " + << Geom::format_coord_nice(eps) + << "\nVector: " << l_expr + << "\nindex " << i << " contains: " << l[i] + << "\nVector:" << r_expr + << "\nindex " << i << " contains: " << r[i] << std::endl; + } + } + return ::testing::AssertionSuccess(); +} + +#define EXPECT_vector_equal(a, b) EXPECT_PRED_FORMAT2(VectorEqual, a, b) +#define EXPECT_vector_near(a, b, eps) EXPECT_PRED_FORMAT3(VectorNear, a, b, eps) + + + +template <typename TA, typename TB> +::testing::AssertionResult IntersectionsValid( + char const *l_expr, char const *r_expr, const char */*xs_expr*/, const char */*eps_expr*/, + TA const &shape_a, TB const &shape_b, + std::vector<Geom::Intersection<typename Geom::ShapeTraits<TA>::TimeType, + typename Geom::ShapeTraits<TB>::TimeType> > const &xs, + Geom::Coord eps) +{ + std::ostringstream os; + bool failed = false; + + for (unsigned i = 0; i < xs.size(); ++i) { + Geom::Point pa = shape_a.pointAt(xs[i].first); + Geom::Point pb = shape_b.pointAt(xs[i].second); + if (!Geom::are_near(pa, xs[i].point(), eps) || + !Geom::are_near(pb, xs[i].point(), eps) || + !Geom::are_near(pb, pa, eps)) + { + os << "Intersection " << i << " does not match\n" + << Geom::format_coord_nice(xs[i].first) << " evaluates to " << pa << "\n" + << Geom::format_coord_nice(xs[i].second) << " evaluates to " << pb << "\n" + << "Reported intersection point is " << xs[i].point() << std::endl; + failed = true; + } + } + + if (failed) { + return ::testing::AssertionFailure() + << "Intersections do not match\n" + << "Shape A: " << l_expr << "\n" + << "Shape B: " << r_expr << "\n" + << os.str() + << "Threshold: " << Geom::format_coord_nice(eps) << std::endl; + } + + return ::testing::AssertionSuccess(); +} + +#define EXPECT_intersections_valid(a, b, xs, eps) EXPECT_PRED_FORMAT4(IntersectionsValid, a, b, xs, eps) diff --git a/tests/timing-test.cpp b/tests/timing-test.cpp new file mode 100644 index 0000000..b5714f7 --- /dev/null +++ b/tests/timing-test.cpp @@ -0,0 +1,270 @@ +#include <sys/time.h> +#include <iostream> +#include <sstream> +#include <vector> +#include <algorithm> +#include <assert.h> +#include <time.h> +#include <sched.h> +#include <math.h> + +const long long US_PER_SECOND = 1000000L; +const long long NS_PER_US = 1000L; + +using namespace std; + +class Timer{ +public: + Timer() {} + // note that CPU time is tracked per-thread, so the timer is only useful + // in the thread it was start()ed from. + void start() { + usec(start_time); + } + void lap(long long &us) { + usec(us); + us -= start_time; + } + long long lap() { + long long us; + usec(us); + return us - start_time; + } + void usec(long long &us) { + clock_gettime(clock, &ts); + us = ts.tv_sec * US_PER_SECOND + ts.tv_nsec / NS_PER_US; + } + /** Ask the OS nicely for a big time slice */ + void ask_for_timeslice() { + sched_yield(); + } +private: + long long start_time; + struct timespec ts; +#ifdef _POSIX_THREAD_CPUTIME + static const clockid_t clock = CLOCK_THREAD_CPUTIME_ID; +#else +# ifdef CLOCK_MONOTONIC + static const clockid_t clock = CLOCK_MONOTONIC; +# else + static const clockid_t clock = CLOCK_REALTIME; +# endif +#endif +}; + +int estimate_useful_window() +{ + Timer tm; + tm.ask_for_timeslice(); + int window = 1; + + while(1) { + tm.start(); + for(int i = 0; i < window; i++) {} + long long base_line = tm.lap(); + if(base_line > 1 and window > 100) + return window; + window *= 2; + } +} + +template <typename T> +string robust_timer(T &t) { + static int base_rate = estimate_useful_window(); + //cout << "base line iterations:" << base_rate << endl; + double sum = 0; + vector<double> results; + const int n_trials = 20; + results.reserve(n_trials); + for(int trials = 0; trials < n_trials; trials++) { + Timer tm; + tm.ask_for_timeslice(); + tm.start(); + int iters = 0; + while(tm.lap() < 10000) { + for(int i = 0; i < base_rate; i++) + t(); + iters+=base_rate; + } + base_rate = iters; + double lap_time = double(tm.lap()); + double individual_time = lap_time/base_rate; + sum += individual_time; + results.push_back(individual_time); + //cout << individual_time << endl; + } + double resS = 0; + double resN = 0; + sort(results.begin(), results.end()); + double ave = results[results.size()/2];//sum/n_trials; // median + //cout << "median:" << ave << endl; + double least = ave; + double resSS = 0; + for(int i = 0; i < n_trials; i++) { + double dt = results[i]; + if(dt <= ave*1.1) { + resS += dt; + resN += 1; + resSS += dt*dt; + if(least < dt) + least = dt; + } + } + + double filtered_ave = resS / resN; + double stddev = sqrt((resSS - 2*resS*filtered_ave + resN*filtered_ave*filtered_ave)/(resN-1)); // sum(x-u)^2 = sum(x^2-2xu+u*u) + assert (least > filtered_ave*0.7); // If this throws something was really screwy + std::basic_stringstream<char> ss; + ss << filtered_ave << " +/-" << stddev << "us"; + return ss.str(); +} + +struct nop{ + void operator()() const {} +}; + +#define degenerate_imported 1 +#include "degenerate.cpp" +using namespace Geom; + +template <typename T> +struct copy{ + T a, b; + void operator()() { + T c = a; + } +}; + +template <typename T> +struct add{ + T a, b; + void operator()() { + T c = a + b; + } +}; + +template <typename T> +struct add_mutate{ + T a, b; + void operator()() { + a += b; + } +}; + +template <typename T> +struct scale{ + T a; + double b; + void operator()() { + T c = a * b; + } +}; + +template <typename T> +struct scale_mutate{ + T a; + double b; + void operator()() { + a *= b; + } +}; + +template <typename T> +struct mult{ + T a, b; + void operator()() { + T c = a * b; + } +}; + +template <typename T> +struct mult_mutate{ + T a, b, c; + void operator()() { + c = a; + c *= b; + } +}; + +template <typename T> +void basic_arith(T const & a, T const & b) { + { + ::copy<T> A; + A.a = a; + A.b = b; + cout << "copy:" + << robust_timer(A) << endl; + } + { + add<T> A; + A.a = a; + A.b = b; + cout << "add:" + << robust_timer(A) << endl; + } + { + add_mutate<T> A; + A.a = a; + A.b = b; + cout << "add_mutate:" + << robust_timer(A) << endl; + } + { + ::scale<T> A; + A.a = a; + A.b = 1; + cout << "scale:" + << robust_timer(A) << endl; + } + { + scale_mutate<T> A; + A.a = a; + A.b = 1; + cout << "scale_mutate:" + << robust_timer(A) << endl; + } + { + mult<T> A; + A.a = a; + A.b = b; + cout << "mult:" + << robust_timer(A) << endl; + } + { + mult_mutate<T> A; + A.a = a; + A.b = b; + cout << "mult_mutate:" + << robust_timer(A) << endl; + } + +} + +#include <valarray> +#include <2geom/orphan-code/sbasisN.h> +#include <2geom/piecewise.h> +int main(int /*argc*/, char** /*argv*/) { + + { + nop N; + cout << "nop:" << robust_timer(N) << endl; + } + + vector<SBasis> sbs; + valarray<double> va(4), vb(4); + generate_random_sbasis(sbs); + cout << "double\n"; + basic_arith(sbs[0][0][0], sbs[1][0][0]); + cout << "valarray\n"; + basic_arith(va, vb); + //cout << "Linear\n"; + //basic_arith(sbs[0][0], sbs[1][0]); + cout << "SBasis\n"; + basic_arith(sbs[0], sbs[1]); + cout << "pw<SBasis>\n"; + basic_arith(Piecewise<SBasis>(sbs[0]), Piecewise<SBasis>(sbs[1])); + /*cout << "SBasisN<1>\n"; + SBasisN<1> sbnA = sbs[0]; + SBasisN<1> sbnB = sbs[0]; + basic_arith(sbnA, sbnB);*/ +} diff --git a/tests/utest.h b/tests/utest.h new file mode 100644 index 0000000..eda1eb4 --- /dev/null +++ b/tests/utest.h @@ -0,0 +1,134 @@ +#ifndef SEEN_UTEST_UTEST_H +#define SEEN_UTEST_UTEST_H + +/* Ultra-minimal unit testing framework */ +/* This file is in the public domain */ + +#ifdef __cplusplus +extern "C" { +#endif +#include <stdlib.h> +#include <stdio.h> +#include <setjmp.h> +//#include <glib/gstrfuncs.h> /* g_strdup_printf */ +#ifdef __cplusplus +}; +#endif + +jmp_buf utest__jmp_buf; +int utest__tests; +int utest__passed; +int utest__running; +const char *utest__name; + +/** \brief Initializes the framework for running a series of tests. + * \param name A descriptive label for this series of tests. + */ +void utest_start(const char *name) { + printf("Testing %s...\n", name); + utest__name = name; + utest__tests = utest__passed = 0; + utest__running = 0; +} + +void utest__pass(void) { + utest__passed++; + utest__running = 0; + printf("OK\n"); +} + + +/** \brief Write \a a, \a b, \a c, and exit the current block of tests. + * + * In the current implementation, any of \a a, \a b, \a c may be NULL, considered equivalent to + * empty string; but don't rely on that unless you also change this documentation string. (No + * callers use this functionality at the time of writing.) + * + * No newline needed in the arguments. + */ +int +utest__fail(const char *a, const char *b, const char *c) +{ + utest__running = 0; + fflush(stdout); + fprintf (stderr, "%s%s%s\n", + (a ? a : ""), + (b ? b : ""), + (c ? c : "")); + fflush(stderr); + longjmp(utest__jmp_buf, 0); + return 0; +} + + +/** \brief Marks a C block constituting a single test. + * \param name A descriptive name for this test. + * + * The block effectively becomes a try statement; if code within the + * block triggers an assertion, control will resume at the end of the + * block. + */ +#define UTEST_TEST(name) if (!setjmp(utest__jmp_buf)&&utest__test((name))) + +/** \brief Terminates the current test if \a cond evaluates to nonzero. + * \param cond The condition to test. + */ +#define UTEST_ASSERT(cond) UTEST_NAMED_ASSERT( #cond, (cond)) + +/** \brief Terminates the current tests if \a _cond evaluates to nonzero, + * and prints a descriptive \a _name instead of the condition + * that caused it to fail. + * \param _name The descriptive label to use. + * \param _cond The condition to test. + */ +#define UTEST_NAMED_ASSERT(_name, _cond) static_cast<void>((_cond) || utest__fail("Assertion `", (_name), "' failed")) + +#define UTEST_ASSERT_SHOW(_cond, _printf_args) \ + static_cast<void>((_cond) \ + || (utest__fail("\nAssertion `" #_cond "' failed; ", "", \ + g_strdup_printf _printf_args))) + +int utest__test(const char *name) { + utest__tests++; + if (utest__running) { + utest__pass(); + } + printf("\t%s...", name); + fflush(stdout); + utest__running = 1; + return 1; +} + +/** \brief Ends a series of tests, reporting test statistics. + * + * Test statistics are printed to stdout or stderr, then the function returns + * nonzero iff all the tests have passed, zero otherwise. + */ +int utest_end(void) { + if (utest__running) { + utest__pass(); + } + if ( utest__passed == utest__tests ) { + printf("%s: OK (all %d passed)\n", + utest__name, utest__tests); + return 1; + } else { + fflush(stdout); + fprintf(stderr, "%s: FAILED (%d/%d tests passed)\n", + utest__name, utest__passed, utest__tests); + fflush(stderr); + return 0; + } +} + + +#endif /* !SEEN_UTEST_UTEST_H */ + +/* + Local Variables: + mode:c + c-file-style:"linux" + fill-column:99 + End: +*/ +// vim: filetype=c:noexpandtab:shiftwidth=8:tabstop=8:fileencoding=utf-8:textwidth=99 : |