├── images └── title.png ├── .gitignore ├── testcases ├── geometry │ ├── lines_inner_contour │ │ └── in.svg │ ├── lines03 │ │ └── in.svg │ ├── lines02 │ │ └── in.svg │ ├── lines_convex02 │ │ └── in.svg │ ├── lines_overlapping_top │ │ └── in.svg │ ├── lines_overlapping_bottom │ │ └── in.svg │ ├── curves01 │ │ └── in.svg │ ├── lines05 │ │ └── in.svg │ ├── lines_overlapping_left │ │ └── in.svg │ ├── lines_overlapping_right │ │ └── in.svg │ ├── curves_special01 │ │ └── in.svg │ ├── curves_special02 │ │ └── in.svg │ ├── curves02 │ │ └── in.svg │ ├── lines_selfintersecting │ │ └── in.svg │ ├── lines_overlapping │ │ └── in.svg │ ├── lines04 │ │ └── in.svg │ ├── curves04 │ │ └── in.svg │ ├── lines01 │ │ └── in.svg │ ├── lines_overlapping_edge │ │ └── in.svg │ ├── lines_overlapping_point │ │ └── in.svg │ ├── lines_almost_overlapping │ │ └── in.svg │ ├── lines_convex01 │ │ └── in.svg │ ├── curves_square_circle02 │ │ └── in.svg │ ├── curves_square_circle04 │ │ └── in.svg │ ├── curves_square_circle01 │ │ └── in.svg │ ├── curves_square_circle03 │ │ └── in.svg │ ├── curves_circles01 │ │ └── in.svg │ ├── curves_circles02 │ │ └── in.svg │ ├── curves06 │ │ └── in.svg │ ├── curves_curved_rectangles01 │ │ └── in.svg │ ├── curves_curved_rectangles02 │ │ └── in.svg │ ├── curves_nontrivial03 │ │ └── in.svg │ ├── curves03 │ │ └── in.svg │ ├── curves_nontrivial02 │ │ └── in.svg │ ├── curves05 │ │ └── in.svg │ ├── curves_nontrivial01 │ │ └── in.svg │ └── curves_nontrivial04 │ │ └── in.svg └── various │ └── bezier_ordering │ └── in.svg ├── CMakeLists.txt ├── examples ├── example1.cpp └── example2.cpp ├── scripts ├── update_testfile.py ├── add_testcase.py └── single_header.py ├── THIRDPARTY.txt ├── test ├── acceptance.cpp ├── geometry_generation.hpp ├── svg_testcases.cpp ├── test_utilities.hpp └── unit.cpp ├── include ├── contour_postprocessing.hpp ├── direct_solvers.hpp ├── polynomial_solver.hpp ├── sweep_point.hpp ├── svg_io.hpp └── geometry_base.hpp └── README.md /images/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/verven/contourklip/HEAD/images/title.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # CLion work directory 2 | .idea/ 3 | 4 | # CLion build directories 5 | cmake-build-*/ 6 | 7 | # Exclude MacOS Finder files. 8 | .DS_Store 9 | 10 | # Don't keep generated svgs. 11 | *.svg 12 | !in.svg -------------------------------------------------------------------------------- /testcases/geometry/lines_inner_contour/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines03/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines02/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines_convex02/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines_overlapping_top/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines_overlapping_bottom/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves01/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines05/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines_overlapping_left/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines_overlapping_right/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_special01/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_special02/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves02/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines_selfintersecting/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines_overlapping/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines04/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves04/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines01/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines_overlapping_edge/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines_overlapping_point/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines_almost_overlapping/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/lines_convex01/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_square_circle02/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_square_circle04/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_square_circle01/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_square_circle03/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_circles01/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_circles02/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | project(contourklip) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(BINARY ${CMAKE_PROJECT_NAME}) 6 | 7 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wpedantic -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer") 8 | 9 | include_directories(include) 10 | include_directories(single_include) 11 | include_directories(test/thirdparty) 12 | 13 | add_executable(${BINARY}_unittst test/unit.cpp 14 | test/test_utilities.hpp 15 | # ${include_files} 16 | include/geometry_base.hpp 17 | include/bezier_utils.hpp 18 | include/svg_io.hpp 19 | include/sweep_point.hpp 20 | include/polyclip.hpp 21 | ) 22 | add_executable(${BINARY}_acceptancetst test/acceptance.cpp) 23 | add_executable(${BINARY}_test_svg test/svg_testcases.cpp) 24 | add_executable(${BINARY}_example examples/example1.cpp) 25 | add_executable(${BINARY}_example2 examples/example2.cpp) -------------------------------------------------------------------------------- /testcases/geometry/curves06/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_curved_rectangles01/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_curved_rectangles02/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_nontrivial03/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves03/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_nontrivial02/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/example1.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "contourklip.hpp" 3 | 4 | int main() { 5 | contourklip::Contour contour1{{0, 100}}; 6 | contour1.push_back({50, 100}); 7 | contour1.push_back({77.5, 100}, {100, 77.5}, {100, 50}); 8 | contour1.push_back({100, 22.5}, {77.5, 0}, {50, 0}); 9 | contour1.push_back({0, 0}); 10 | contour1.close(); 11 | 12 | contourklip::Contour contour2{{150, 25}}; 13 | contour2.push_back({100, 25}); 14 | contour2.push_back({72.3, 25}, {50, 47.3}, {50, 75}); 15 | contour2.push_back({50, 102.5}, {72.3, 125}, {100, 125}); 16 | contour2.push_back({150, 125}); 17 | contour2.close(); 18 | 19 | std::vector shape1{contour1}; 20 | std::vector shape2{contour2}; 21 | std::vector result{}; 22 | 23 | if(contourklip::clip(shape1, shape2, result, contourklip::INTERSECTION)){ 24 | std::cout << "clipping operation succeeded\n"; 25 | } 26 | 27 | for (const auto &contour: result) { 28 | std::cout << "contour:\n"; 29 | std::cout << contour; 30 | } 31 | return 0; 32 | } -------------------------------------------------------------------------------- /scripts/update_testfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | ''' 5 | generates the file svg_testcases.hpp. 6 | ''' 7 | 8 | TESTCASES_SVG_DIR = "testcases/geometry" 9 | TESTCASE_NAME = "test/svg_testcases.cpp" 10 | 11 | def construct_tst_case(name): 12 | 13 | return f'''TEST_CASE("{name}"){{\n do_test("{name}");\n}}\n\n''' 14 | 15 | def construct_tst_file(): 16 | subfolders = [Path(f.path).name for f in os.scandir(TESTCASES_SVG_DIR) if f.is_dir() ] 17 | subfolders.sort(reverse=True) 18 | 19 | cases_str= ''.join([construct_tst_case(i) for i in subfolders]) 20 | 21 | includes = '''#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN\n#include "test_utilities.hpp"\n#include "doctest.h"\n\n''' 22 | 23 | tst_start = 'TEST_SUITE_BEGIN("svg geometry testcases");\n\n' 24 | comment = "//This file was auto-generated from `scripts/update_testfile.py`.\n\n" 25 | 26 | tst_end = "TEST_SUITE_END;\n" 27 | 28 | return comment + includes + tst_start+ cases_str + tst_end 29 | 30 | 31 | write_p = Path(TESTCASE_NAME) 32 | with write_p.open("w", encoding="utf-8") as f: 33 | f.write(construct_tst_file()) 34 | 35 | -------------------------------------------------------------------------------- /testcases/geometry/curves05/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /testcases/geometry/curves_nontrivial01/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /THIRDPARTY.txt: -------------------------------------------------------------------------------- 1 | This project includes the following subcomponents that are subject to separate license terms. 2 | 3 | doctest 4 | https://github.com/doctest/doctest 5 | License: MIT 6 | the notice below can be obtained at https://github.com/doctest/doctest/blob/master/LICENSE.txt 7 | 8 | The MIT License (MIT) 9 | 10 | Copyright (c) 2016-2021 Viktor Kirilov 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. -------------------------------------------------------------------------------- /test/acceptance.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include "doctest.h" 3 | #include "polyclip.hpp" 4 | #include "test_utilities.hpp" 5 | 6 | TEST_CASE("tiny contour fuzzy only lines"){ 7 | for (int seed = 1; seed < 10000; ++seed) { 8 | contourklip::Contour c1, c2; 9 | geometrygen::generate_contour(10, 2., 8, 1.5, seed, c1, {}, false); 10 | geometrygen::generate_contour(10, 2., 8, 1.5, seed + 1, c2, {0.25, 1}, false); 11 | test_ops(std::vector{c1}, std::vector{c2}, true); 12 | } 13 | } 14 | 15 | TEST_CASE("tiny contour fuzzy tests"){ 16 | int k = 100; 17 | for (int seed = 1; seed < k; ++seed) { 18 | for (int seed2 = seed+1; seed2 < k; ++seed2) { 19 | contourklip::Contour c1, c2; 20 | geometrygen::generate_contour(5, 2., 8, 1.5, seed, c1, {}, true); 21 | geometrygen::generate_contour(5, 2., 8, 1.5, seed2, c2, {0.25, 1}, true); 22 | test_ops(std::vector{c1}, std::vector{c2}, true); 23 | } 24 | } 25 | } 26 | 27 | TEST_CASE("disallowed contour"){ 28 | contourklip::Contour c1{}; 29 | c1.push_back({0, 1}); 30 | c1.push_back({0, 2}); 31 | c1.push_back({-1, 2}); 32 | c1.push_back({2, 2}); 33 | c1.close(); 34 | 35 | contourklip::Contour c2{}; 36 | geometrygen::generate_rectangle({-5, 5}, {1, -2}, c2); 37 | std::vector out{}; 38 | bool res = contourklip::clip(c2, c1, out, contourklip::DIFFERENCE); 39 | 40 | CHECK( ! res); 41 | } 42 | 43 | TEST_CASE("disallowed contour 2"){ 44 | contourklip::Contour c1{}; 45 | c1.push_back({0, 0}); 46 | c1.push_back({0, 1}); 47 | c1.push_back({2, 2}, {0, 2}, {2, 1}); 48 | c1.push_back({2, 0}); 49 | c1.close(); 50 | std::vector out{}; 51 | bool res = contourklip::clip(c1, {}, out, contourklip::DIFFERENCE); 52 | 53 | CHECK( ! res); 54 | } -------------------------------------------------------------------------------- /testcases/various/bezier_ordering/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/example2.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "contourklip.hpp" 3 | 4 | int main() { 5 | contourklip::Contour contour1{{0, 100}}; 6 | contour1.push_back({50, 100}); 7 | contour1.push_back({77.5, 100}, {100, 77.5}, {100, 50}); 8 | contour1.push_back({100, 22.5}, {77.5, 0}, {50, 0}); 9 | contour1.push_back({0, 0}); 10 | contour1.close(); 11 | contourklip::Contour contour2{{150, 25}}; 12 | contour2.push_back({100, 25}); 13 | contour2.push_back({72.3, 25}, {50, 47.3}, {50, 75}); 14 | contour2.push_back({50, 102.5}, {72.3, 125}, {100, 125}); 15 | contour2.push_back({150, 125}); 16 | contour2.close(); 17 | std::vector shape1{contour1}; 18 | std::vector shape2{contour2}; 19 | std::vector result{}; 20 | 21 | //callback for determining if a is on the left of the segment p0, p1. 22 | // here it just calls the default callback. 23 | auto above = 24 | [](const contourklip::Point2d &p0, 25 | const contourklip::Point2d &p1, const contourklip::Point2d &a) -> bool{ 26 | return contourklip::detail::LeftOfLine{}(p0, p1, a); 27 | }; 28 | 29 | // callback for determining if 3 points are collinear. 30 | // Again this just calls the default implementation. 31 | auto collinear = 32 | [](const contourklip::Point2d &p0, 33 | const contourklip::Point2d &p1, const contourklip::Point2d &p2) -> bool{ 34 | return contourklip::detail::IsCollinear{}(p0, p1, p2); 35 | }; 36 | 37 | contourklip::Config c; 38 | c.postprocess = false; 39 | 40 | contourklip::PolyClip clip{shape1, shape2, result, 41 | contourklip::INTERSECTION, c}; 42 | 43 | clip.compute(); 44 | 45 | if(clip.success()){ 46 | std::cout << "clipping operation succeeded\n"; 47 | } 48 | return 0; 49 | } -------------------------------------------------------------------------------- /scripts/add_testcase.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pathlib 3 | import argparse 4 | 5 | def svg_prefix(viewBox): 6 | 7 | return f'\n\n' 8 | 9 | def svg_suffix(): 10 | return "" 11 | 12 | 13 | 14 | def to_testcase_svg(svg_str): 15 | svg_str = svg_str.strip() 16 | path_re = "" 17 | path_re= re.compile(path_re) 18 | viewbox_re = r"svg(?:.*?)viewBox=\"(.*?)\"(?:.*?)" 19 | viewbox_re = re.compile(viewbox_re) 20 | 21 | viewbox_vals = re.search(viewbox_re, svg_str) 22 | if not viewbox_vals: 23 | print("couldn't infer svg dimensions") 24 | exit(1) 25 | t= viewbox_vals.group(1).strip().split()[-2:] 26 | height, width = int(t[0]), int(t[1]) 27 | 28 | out= svg_prefix(viewbox_vals.group(1)) 29 | 30 | for idx, p in enumerate(re.finditer(path_re, svg_str)): 31 | print(p.group(1), "\n\n") 32 | out += f'\n' 33 | # if idx ==1: 34 | # break 35 | 36 | return out +svg_suffix() 37 | 38 | 39 | 40 | if __name__ == "__main__": 41 | 42 | parser = argparse.ArgumentParser(description='Process an svg file so that it creates a testcase svg file.') 43 | parser.add_argument('filepath', metavar='file', type=str, help='the filepath to the svg file') 44 | args = parser.parse_args() 45 | p= args.filepath 46 | 47 | if not p.endswith(".svg"): 48 | print("the file is not an .svg file") 49 | exit(1) 50 | out_dir = pathlib.Path(p[:-4]) 51 | out_dir.mkdir(parents=True, exist_ok=True) 52 | write_p = out_dir / "in.svg" 53 | with open(p) as file_in: 54 | svg_str = file_in.read() 55 | with write_p.open("w", encoding="utf-8") as f: 56 | f.write(to_testcase_svg(svg_str)) 57 | -------------------------------------------------------------------------------- /include/contour_postprocessing.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONTOURKLIP_CONTOUR_POSTPROCESSING_HPP 2 | #define CONTOURKLIP_CONTOUR_POSTPROCESSING_HPP 3 | 4 | #include 5 | #include "geometry_base.hpp" 6 | 7 | namespace contourklip::detail { 8 | template 9 | void postprocess_contour(Contour &c, outF &report_contour, bool remove_collinear = true, 10 | CollinearF collinear_f = {}) { 11 | if (c.size() <= 3) { 12 | report_contour(c); 13 | return; 14 | } 15 | 16 | std::map visited_p{}; 17 | std::vector skip(c.size(), 0); 18 | std::fill(skip.begin(), skip.end(), 0); 19 | 20 | std::size_t prev; 21 | for (std::size_t i = 0; i < c.size(); ++i) { 22 | auto v = visited_p.find(c[i].point()); 23 | if (v != visited_p.end()) { 24 | prev = v->second; 25 | // update the value to be the last updated 26 | v->second = i; 27 | // it could also be that we have an overl. point of a sub-contour that is already deleted. 28 | if (skip[prev] != 0) { 29 | continue; 30 | } 31 | 32 | Contour subcontour{}; 33 | //we still need to keep one of the 2 points on the contour 34 | for (std::size_t j = prev; j < i; ++j) { 35 | if (remove_collinear && subcontour.size() >= 2 36 | && !subcontour.back().bcurve() && !c[j].bcurve() 37 | && collinear_f(subcontour[subcontour.size() - 2].point(), 38 | subcontour.back_point(), 39 | c[j].point()) 40 | ) { 41 | subcontour.back().point() = c[j].point(); 42 | } else { 43 | subcontour.push_back(c[j]); 44 | } 45 | if (skip[j]) { 46 | j = skip[j]; 47 | } 48 | skip[j] = i; 49 | } 50 | subcontour.push_back(c[i]); 51 | if (subcontour.size() <= 2) { 52 | continue; 53 | } 54 | report_contour(subcontour); 55 | } 56 | // current point is new 57 | else { 58 | visited_p.insert({c[i].point(), i}); 59 | } 60 | } 61 | } 62 | } 63 | #endif //CONTOURKLIP_CONTOUR_POSTPROCESSING_HPP -------------------------------------------------------------------------------- /scripts/single_header.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import textwrap 3 | 4 | ''' 5 | This script creates the single-header file. 6 | It is quite manual since it does not automatically collect and sort the dependencies. 7 | it uses the utility "unifdef" (https://dotat.at/prog/unifdef/) which is essentially a 8 | preprocessor which can conditionally define / undefine symbols. Unlike eg. gcc -E, it will 9 | only expand macros and not modify the rest, so it is suitable to generate a readable header. 10 | ''' 11 | 12 | PREAMBLE = ''' 13 | /* 14 | Contourklip, a contour clipping library which supports cubic beziers. 15 | 16 | Copyright (C) 2022 verven [ vervencode@protonmail.com ] 17 | 18 | This program is free software: you can redistribute it and/or modify 19 | it under the terms of the GNU General Public License as published by 20 | the Free Software Foundation, either version 3 of the License, or 21 | (at your option) any later version. 22 | 23 | This program is distributed in the hope that it will be useful, 24 | but WITHOUT ANY WARRANTY; without even the implied warranty of 25 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26 | GNU General Public License for more details. 27 | 28 | You should have received a copy of the GNU General Public License 29 | along with this program. If not, see . 30 | */ 31 | 32 | ''' 33 | 34 | ROOT_HEADER = "include/polyclip.hpp" 35 | OUTPUT = "single_include/contourklip.hpp" 36 | HEADER_NAME = "CONTOURKLIP_CONTOURKLIP_HPP" 37 | 38 | # combine files 39 | # unifdef -o single_include/contourklip.hpp -UDEBUG -DCONTOURKLIP_SVG_IO_HPP single_include/contourklip.hpp 40 | UDEBUG_COMMAND = ['unifdef', '-o', OUTPUT, '-UDEBUG', '-DCONTOURKLIP_SVG_IO_HPP', OUTPUT] 41 | 42 | ORDERED_FILES = [ f"include/{i}" for i in [ 43 | "direct_solvers.hpp", 44 | "polynomial_solver.hpp", 45 | "geometry_base.hpp", 46 | "bezier_utils.hpp", 47 | "sweep_point.hpp", 48 | "contour_postprocessing.hpp", 49 | "polyclip.hpp", 50 | ] 51 | ] 52 | 53 | 54 | with open(OUTPUT, 'w+') as outfile: 55 | for f in ORDERED_FILES: 56 | outfile.write("\n") 57 | with open(f) as infile: 58 | for line in infile: 59 | outfile.write(line) 60 | # 61 | outfile.write("\n") 62 | 63 | subprocess.run(UDEBUG_COMMAND) 64 | 65 | std_includes = set() 66 | data = [] 67 | 68 | with open(OUTPUT, "r+") as outfile: 69 | for line in outfile: 70 | if not "#include" in line: 71 | data.append(line) 72 | if "#include <" in line: 73 | incl = line[:line.find(">")+1].strip() 74 | std_includes.add(incl) 75 | 76 | outfile.seek(0) 77 | outfile.truncate() 78 | outfile.write(PREAMBLE + "#ifndef " + HEADER_NAME + "\n#define " + HEADER_NAME 79 | + "\n" ) 80 | for i in sorted(std_includes): 81 | outfile.write(f"{i}\n") 82 | for i in data: 83 | outfile.write(i) 84 | outfile.write("\n#endif //" + HEADER_NAME) -------------------------------------------------------------------------------- /test/geometry_generation.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONTOURKLIP_GEOMETRY_GENERATION_HPP 2 | #define CONTOURKLIP_GEOMETRY_GENERATION_HPP 3 | 4 | #include 5 | #include "geometry_base.hpp" 6 | 7 | namespace geometrygen{ 8 | struct UniformF{ 9 | std::mt19937 gen; 10 | std::uniform_real_distribution<> d; 11 | UniformF(double a, double b, int seed = 1): d(a, b){ 12 | gen = std::mt19937(seed); 13 | } 14 | double operator()(void){ 15 | return d(gen); 16 | } 17 | }; 18 | 19 | template 20 | class PointGenerator{ 21 | UniformF h, v; 22 | public: 23 | PointGenerator(double xa=0, double xb=1, double ya=0, 24 | double yb=0, int seed=1) : h(xa, xb, seed), v(ya, yb, seed+1) {} 25 | 26 | P operator()(void){ 27 | return P{h(), v()}; 28 | } 29 | }; 30 | 31 | std::vector random_partition(double a, double b, int n, int seed=0){ 32 | UniformF dis(a, b, seed); 33 | 34 | std::vector v; 35 | for (int i = 0; i < n-1; ++i) { 36 | v.push_back(dis()); 37 | } 38 | std::sort(v.begin(), v.end()); 39 | return v; 40 | } 41 | 42 | std::vector random_partition2(double a, double b, int n, int seed=0){ 43 | double interval = (b-a)/(double(n)); 44 | 45 | UniformF dis(0, interval, seed); 46 | 47 | std::vector v{}; 48 | v.reserve(n-1); 49 | for (int i = 1; i < n; ++i) { 50 | v.push_back(dis() + double(i*interval)); 51 | } 52 | return v; 53 | } 54 | 55 | void generate_contour(int n, double a, double b, double c, int seed, contourklip::Contour& out, contourklip::Point2d offset = {0, 0}, bool withcurves = true){ 56 | UniformF rand_dist(a, b, seed); 57 | UniformF rand_offset(std::max(0., b-c), b+c, seed+1); 58 | 59 | auto partition = random_partition2(0, 2*M_PI, n, seed+2); 60 | double prev_angle =0; 61 | double d = rand_dist(); 62 | out.push_back({d + offset.x(), 0+offset.y()}); 63 | 64 | auto frompolar = [&offset](double r, double angle) -> contourklip::Point2d{ 65 | return {r*std::cos(angle) + offset.x(), r * std::sin(angle) + offset.y()}; 66 | }; 67 | int k =0; 68 | for (const auto &angle: partition) { 69 | d = rand_dist(); 70 | if (withcurves && (rand_dist() < a + 0.5*(b-a))){ 71 | UniformF rand_angle(prev_angle, angle, seed); 72 | double a1 = rand_angle(), a2 = rand_angle(); 73 | if(a1 > a2) std::swap(a1, a2); 74 | double d1 = rand_offset(), d2 = rand_offset(); 75 | out.push_back(frompolar(d1, a1), 76 | frompolar(d2, a2), frompolar(d, angle)); 77 | }else{ 78 | out.push_back(frompolar(d, angle)); 79 | } 80 | prev_angle = angle; 81 | k++; 82 | } 83 | out.close(); 84 | } 85 | 86 | void generate_rectangle(const contourklip::Point2d& a, const contourklip::Point2d& b, contourklip::Contour& out){ 87 | out.push_back(a); 88 | out.push_back({a.x(), b.y()}); 89 | out.push_back(b); 90 | out.push_back({b.x(), a.y()}); 91 | out.close(); 92 | } 93 | 94 | }; 95 | #endif //CONTOURKLIP_GEOMETRY_GENERATION_HPP -------------------------------------------------------------------------------- /test/svg_testcases.cpp: -------------------------------------------------------------------------------- 1 | //This file was auto-generated from `scripts/update_testfile.py`. 2 | 3 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 4 | #include "test_utilities.hpp" 5 | #include "doctest.h" 6 | 7 | TEST_SUITE_BEGIN("svg geometry testcases"); 8 | 9 | TEST_CASE("lines_selfintersecting"){ 10 | do_test("lines_selfintersecting"); 11 | } 12 | 13 | TEST_CASE("lines_overlapping_top"){ 14 | do_test("lines_overlapping_top"); 15 | } 16 | 17 | TEST_CASE("lines_overlapping_right"){ 18 | do_test("lines_overlapping_right"); 19 | } 20 | 21 | TEST_CASE("lines_overlapping_point"){ 22 | do_test("lines_overlapping_point"); 23 | } 24 | 25 | TEST_CASE("lines_overlapping_left"){ 26 | do_test("lines_overlapping_left"); 27 | } 28 | 29 | TEST_CASE("lines_overlapping_edge"){ 30 | do_test("lines_overlapping_edge"); 31 | } 32 | 33 | TEST_CASE("lines_overlapping_bottom"){ 34 | do_test("lines_overlapping_bottom"); 35 | } 36 | 37 | TEST_CASE("lines_overlapping"){ 38 | do_test("lines_overlapping"); 39 | } 40 | 41 | TEST_CASE("lines_inner_contour"){ 42 | do_test("lines_inner_contour"); 43 | } 44 | 45 | TEST_CASE("lines_convex02"){ 46 | do_test("lines_convex02"); 47 | } 48 | 49 | TEST_CASE("lines_convex01"){ 50 | do_test("lines_convex01"); 51 | } 52 | 53 | TEST_CASE("lines_almost_overlapping"){ 54 | do_test("lines_almost_overlapping"); 55 | } 56 | 57 | TEST_CASE("lines05"){ 58 | do_test("lines05"); 59 | } 60 | 61 | TEST_CASE("lines04"){ 62 | do_test("lines04"); 63 | } 64 | 65 | TEST_CASE("lines03"){ 66 | do_test("lines03"); 67 | } 68 | 69 | TEST_CASE("lines02"){ 70 | do_test("lines02"); 71 | } 72 | 73 | TEST_CASE("lines01"){ 74 | do_test("lines01"); 75 | } 76 | 77 | TEST_CASE("curves_square_circle04"){ 78 | do_test("curves_square_circle04"); 79 | } 80 | 81 | TEST_CASE("curves_square_circle03"){ 82 | do_test("curves_square_circle03"); 83 | } 84 | 85 | TEST_CASE("curves_square_circle02"){ 86 | do_test("curves_square_circle02"); 87 | } 88 | 89 | TEST_CASE("curves_square_circle01"){ 90 | do_test("curves_square_circle01"); 91 | } 92 | 93 | TEST_CASE("curves_special02"){ 94 | do_test("curves_special02"); 95 | } 96 | 97 | TEST_CASE("curves_special01"){ 98 | do_test("curves_special01"); 99 | } 100 | 101 | TEST_CASE("curves_nontrivial04"){ 102 | do_test("curves_nontrivial04"); 103 | } 104 | 105 | TEST_CASE("curves_nontrivial03"){ 106 | do_test("curves_nontrivial03"); 107 | } 108 | 109 | TEST_CASE("curves_nontrivial02"){ 110 | do_test("curves_nontrivial02"); 111 | } 112 | 113 | TEST_CASE("curves_nontrivial01"){ 114 | do_test("curves_nontrivial01"); 115 | } 116 | 117 | TEST_CASE("curves_curved_rectangles02"){ 118 | do_test("curves_curved_rectangles02"); 119 | } 120 | 121 | TEST_CASE("curves_curved_rectangles01"){ 122 | do_test("curves_curved_rectangles01"); 123 | } 124 | 125 | TEST_CASE("curves_circles02"){ 126 | do_test("curves_circles02"); 127 | } 128 | 129 | TEST_CASE("curves_circles01"){ 130 | do_test("curves_circles01"); 131 | } 132 | 133 | TEST_CASE("curves06"){ 134 | do_test("curves06"); 135 | } 136 | 137 | TEST_CASE("curves05"){ 138 | do_test("curves05"); 139 | } 140 | 141 | TEST_CASE("curves04"){ 142 | do_test("curves04"); 143 | } 144 | 145 | TEST_CASE("curves03"){ 146 | do_test("curves03"); 147 | } 148 | 149 | TEST_CASE("curves02"){ 150 | do_test("curves02"); 151 | } 152 | 153 | TEST_CASE("curves01"){ 154 | do_test("curves01"); 155 | } 156 | 157 | TEST_SUITE_END; 158 | -------------------------------------------------------------------------------- /include/direct_solvers.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONTOURKLIP_DIRECT_SOLVERS_HPP 2 | #define CONTOURKLIP_DIRECT_SOLVERS_HPP 3 | #include 4 | namespace directsolvers { 5 | double clip(double val, double lower, double upper) { 6 | return std::max(lower, std::min(val, upper)); 7 | } 8 | 9 | /* 10 | see: https://stackoverflow.com/questions/63665010 11 | 12 | diff_of_products() computes a*b-c*d with a maximum error <= 1.5 ulp 13 | 14 | Claude-Pierre Jeannerod, Nicolas Louvet, and Jean-Michel Muller, 15 | "Further Analysis of Kahan's Algorithm for the Accurate Computation 16 | of 2x2 Determinants". Mathematics of Computation, Vol. 82, No. 284, 17 | Oct. 2013, pp. 2245-2264 18 | */ 19 | double diff_of_products (double a, double b, double c, double d) { 20 | double w = d * c; 21 | double e = std::fma (-d, c, w); 22 | double f = std::fma (a, b, -w); 23 | return f + e; 24 | } 25 | 26 | 27 | int solve_quadratic(double a_0, double a_1, double a_2, std::pair &r) { 28 | if (std::abs(a_2) < 1e-15) { 29 | if (std::abs(a_1) < 1e-15) { 30 | return 0; 31 | } 32 | r.first = r.second = -a_0 / a_1; 33 | return 1; 34 | } 35 | double d = diff_of_products(a_1, a_1, 4.0*a_2, a_0); 36 | if (d < 0) { 37 | return 0; 38 | } 39 | double sqd = sqrt(d); 40 | double u = 1.0 / a_2; 41 | if (a_1 >= 0.0) { 42 | double t = 0.5 * (-a_1 - sqd) * u; 43 | r.first = t; 44 | r.second = u * a_0 / t; 45 | } else { 46 | double t = 0.5 * (-a_1 + sqd) * u; 47 | r.first = u * a_0 / t; 48 | r.second = t; 49 | } 50 | return 2; 51 | } 52 | 53 | template 54 | void solve_cubic_real(double a_0, double a_1, double a_2, double a_3, RootConsumer &c, double tol) { 55 | //special case: not a cubic, fall back to quadratic 56 | if (std::abs(a_3) < tol) { 57 | std::pair r{}; 58 | int t = solve_quadratic(a_0, a_1, a_2, r); 59 | if (t == 1) { 60 | c(r.first); 61 | } 62 | if (t == 2) { 63 | c(r.first); 64 | c(r.second); 65 | } 66 | return; 67 | } 68 | //normalize so that highest coefficient is 1 69 | a_2 /= a_3; 70 | a_1 /= a_3; 71 | a_0 /= a_3; 72 | 73 | double a2_2 = a_2 * a_2; 74 | double a_2over3 = a_2 / 3.; 75 | double q = a_1 / 3.0 - a2_2 / 9.0; 76 | double r = (a_1 * a_2 - 3. * a_0) / 6.0 - (a2_2 * a_2) / 27.0; 77 | double rr = r * r; 78 | double q3 = q * q * q; 79 | double check = rr + q3; 80 | // case: three real solutions 81 | if (check <= 0 || check < tol) { 82 | double theta = 0; 83 | if (!(abs(q) < tol)) { 84 | double temp = clip(r / sqrt(-q3), -1, 1); 85 | theta = acos(temp); 86 | } 87 | double angle1 = theta / 3.; 88 | double angle2 = angle1 - 2. * M_PI / 3.; 89 | double angle3 = angle1 + 2. * M_PI / 3.; 90 | double sq = 2. * std::sqrt(-q); 91 | double r1, r2, r3; 92 | r1 = sq * cos(angle3) - a_2over3; 93 | r2 = sq * cos(angle2) - a_2over3; 94 | r3 = sq * cos(angle1) - a_2over3; 95 | // it holds that r1 <= r2 <= r3 96 | c(r1); 97 | c(r2); 98 | c(r3); 99 | return; 100 | 101 | } 102 | //only one real solution 103 | double sq = sqrt(check); 104 | double u = cbrt(r + sq); 105 | double v = cbrt(r - sq); 106 | double r1 = u + v - a_2over3; 107 | c(r1); 108 | } 109 | } 110 | #endif //CONTOURKLIP_DIRECT_SOLVERS_HPP -------------------------------------------------------------------------------- /include/polynomial_solver.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONTOURKLIP_POLYNOMIAL_SOLVER_HPP 2 | #define CONTOURKLIP_POLYNOMIAL_SOLVER_HPP 3 | #include 4 | #include 5 | #include 6 | namespace polynomialsolver { 7 | 8 | template 9 | std::optional itp_root_refine(U &func, T a, T b, T eps, int maxiter) { 10 | //leveraging argument dependent lookup 11 | using std::log2; 12 | using std::abs; 13 | using std::exp2; 14 | T a_start = a; 15 | T b_start = b; 16 | T k1 = (T) 0.2 / (b - a); 17 | int nmax = int(log2((b - a) / (2 * eps))) + 2; 18 | int i = 0; 19 | T fa = func(a); 20 | T fb = func(b); 21 | while (b - a > 2 * eps && i < maxiter) { 22 | if(fa ==(T)0){ 23 | return a; 24 | } 25 | if(fb==(T)0){ 26 | return b; 27 | } 28 | //safety check in case interval is degenerate 29 | if(a < a_start || b > b_start) return {}; 30 | if(fa * fb >0) return {}; 31 | T x_mid = (a + b) / 2.0; 32 | T r = eps * (exp2(nmax - i)) - (b - a) / 2.0; 33 | T delta = k1 * (b - a) * (b - a); 34 | T x_f = (fb * a - fa * b) / (fb - fa); 35 | T si = x_mid - x_f; 36 | si = si < 0 ? -1 : 1; 37 | T x_t = (delta <= abs(x_mid - x_f)) ? x_f + si * delta : x_mid; 38 | T x_itp = (abs(x_t - x_mid) <= r) ? x_t : x_mid - si * r; 39 | T f_x = func(x_itp); 40 | if (f_x * fa < 0) { 41 | b = x_itp; 42 | fb = f_x; 43 | } else { 44 | a = x_itp; 45 | fa = f_x; 46 | } 47 | i++; 48 | } 49 | return (T)0.5*(a+b); 50 | } 51 | 52 | template 53 | constexpr T linearinter(T p0, T p1, T t) { 54 | return (1 - t) * p0 + t * p1; 55 | } 56 | 57 | // casteljau subdivision using O(N) memory 58 | template 59 | void casteljau_subdiv(const std::array &coeffs, T t, std::array &res_first, std::array &res_second) { 60 | std::array, 2> table{}; 61 | std::size_t curr = 0, prev = 0; 62 | for (std::size_t i = 0; i < N; ++i) { 63 | table[curr][i] = coeffs[i]; 64 | } 65 | curr = 1; 66 | for (std::size_t i = 1; i < N; ++i) { 67 | for (std::size_t j = 0; j < N - i; ++j) { 68 | table[curr][j] = linearinter(table[prev][j], table[prev][j + 1], t); 69 | } 70 | res_first[i] = table[curr][0]; 71 | res_second[N - i - 1] = table[curr][N - i - 1]; 72 | prev = curr; 73 | curr = (1 - curr); 74 | } 75 | res_first[0] = coeffs[0]; 76 | res_second[N - 1] = coeffs[N - 1]; 77 | } 78 | 79 | // converts a polynomial from the monomial basis given by coeffs to the bezier basis by 80 | // storing the result in out. O(N^2) operation with O(N) memory 81 | template 82 | constexpr void basis_conversion(const std::array &coeffs, std::array &out) { 83 | long bin_c = 1; 84 | std::array, 2> table{}; 85 | for (std::size_t i = 0; i < N; ++i) { 86 | table[0][i] = coeffs[i] / ((T) bin_c); 87 | //careful about operation order 88 | bin_c = (bin_c * ((N - 1) - i)) / (i + 1); 89 | } 90 | 91 | std::size_t curr = 0, prev = 1; 92 | for (std::size_t i = 0; i < N; ++i) { 93 | out[i] = table[curr][0]; 94 | prev = curr; 95 | curr = 1-curr; 96 | for (std::size_t j = 0; j < N - i -1; ++j) { 97 | table[curr][j] = table[prev][j] + table[prev][j +1]; 98 | } 99 | } 100 | } 101 | 102 | // returns the number of sign changes in the coefficients. Numerically zero 103 | // coefficients as given by the functor are ignored. 104 | template 105 | constexpr int sign_changes(const std::array &coeffs, zeroF& is_zero) { 106 | std::size_t start = 0, end = N - 1; 107 | while (start < N - 1 && is_zero(coeffs[start])) { 108 | start++; 109 | } 110 | // start == N -1 || c >0; 111 | while (end > 1 && is_zero(coeffs[end])) { 112 | end--; 113 | } 114 | T prev = coeffs[start]; 115 | int out = 0; 116 | for (std::size_t i = start; i <= end; ++i) { 117 | if (is_zero(coeffs[i]) ) { 118 | continue; 119 | } 120 | if (coeffs[i] * prev < 0) { 121 | out++; 122 | } 123 | prev = coeffs[i]; 124 | } 125 | return out; 126 | } 127 | 128 | // brackets the roots of the polynomial defined by the coeffs array, passes the interval to the callback 129 | template 130 | void rootbracket_bezier(const std::array &coeffs, T a, T b, OutF &process, T abstol) { 131 | using std::abs; 132 | 133 | if (abs(b-a) <= abstol){ 134 | return; 135 | } 136 | auto iszero = [&abstol](T d){ 137 | return abs(d) <= abstol; 138 | }; 139 | bool boundary1 = iszero(coeffs.front()); 140 | bool boundary2 = iszero(coeffs.back()); 141 | 142 | switch (sign_changes(coeffs, iszero)) { 143 | case 0: 144 | if(boundary1 && boundary2) break; 145 | if( boundary2 || (a ==0 && boundary1) ){ 146 | process({a, b}); 147 | } 148 | return; 149 | case 1: 150 | if (!boundary1 && !boundary2) { 151 | process({a, b}); 152 | return; 153 | } 154 | } 155 | T mid = 0.5 * (a + b); 156 | std::array leftcoeffs{}; 157 | std::array rightcoeffs{}; 158 | casteljau_subdiv(coeffs, (T) 0.5, leftcoeffs, rightcoeffs); 159 | rootbracket_bezier(leftcoeffs, a, mid, process, abstol); 160 | rootbracket_bezier(rightcoeffs, mid, b, process, abstol); 161 | } 162 | 163 | // Horner polynomial evaluation scheme. 164 | template 165 | constexpr T polyval(const std::array &coeffs, T t) { 166 | T out = coeffs[N - 1]; 167 | for (int i = N - 2; i >= 0; i--) { 168 | out = coeffs[i] + t * out; 169 | } 170 | return out; 171 | } 172 | 173 | // derivative of polynomial 174 | template 175 | constexpr void poly_der(const std::array &coeffs, std::array &out) { 176 | for (int i = 1; i < N; ++i) { 177 | out[i - 1] = coeffs[i] * i; 178 | } 179 | } 180 | 181 | template 182 | struct PolynomialFunc { 183 | std::array coeffs; 184 | explicit PolynomialFunc(const std::array &poly) : coeffs(poly) {} 185 | 186 | PolynomialFunc derivative(){ 187 | std::array derivative_coeffs; 188 | if constexpr(BezierRepr){ 189 | poly_der_bezier(coeffs, derivative_coeffs); 190 | } else{ 191 | poly_der(coeffs, derivative_coeffs); 192 | } 193 | return {derivative_coeffs}; 194 | } 195 | T operator()(T x) const{ 196 | if constexpr(BezierRepr){ 197 | return polyeval_bezier(coeffs, x); 198 | }else{ 199 | return polyval(coeffs, x); 200 | } 201 | } 202 | }; 203 | 204 | template 205 | void rootbracket(const std::array &poly, IntervalConsumer &out, T abstol = 1e-15) { 206 | using std::abs; 207 | T acc = 0; 208 | for (const auto &c: poly) { 209 | acc += abs(c); 210 | } 211 | if(abs(acc) <= abstol){ 212 | return; 213 | } 214 | std::array bezier_coeffs{}; 215 | basis_conversion(poly, bezier_coeffs); 216 | rootbracket_bezier(bezier_coeffs, (T) 0, (T) 1, out, abstol); 217 | } 218 | 219 | // numerically computes roots of the input coeffs, given in monomial basis form. 220 | template 221 | void getpolyroots(const std::array &poly, RootConsumer &out, 222 | int niter = 25, T abstol = 1e-15, T interval_eps = 1e-20) { 223 | //Note that intervals are added in increasing order 224 | std::size_t numroots =0; 225 | std::array, N> root_intervals{}; 226 | auto add = [&](const std::pair& interval){ 227 | if(numroots < N) root_intervals[numroots++] = interval; 228 | }; 229 | using std::abs; 230 | rootbracket(poly, add, abstol); 231 | PolynomialFunc poly_function{poly}; 232 | for (std::size_t i = 0; i < numroots; ++i) { 233 | auto [a, b] = root_intervals[i]; 234 | // by assumption each interval contains exactly 1 root 235 | if(abs(poly_function(a)) 252 | std::array poly_from_roots(const std::array roots) { 253 | std::array out{}; 254 | out[0] = -roots[0]; 255 | out[1] = 1; 256 | for (std::size_t i = 1; i < roots.size(); ++i) { 257 | for (int j = i; j >= 0; --j) { 258 | out[j + 1] = out[j]; 259 | } 260 | out[0] = 0; 261 | for (int j = 0; j < i + 1; ++j) { 262 | out[j] += out[j + 1] * -roots[i]; 263 | } 264 | } 265 | return out; 266 | } 267 | } 268 | #endif //CONTOURKLIP_POLYNOMIAL_SOLVER_HPP -------------------------------------------------------------------------------- /include/sweep_point.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONTOURKLIP_SWEEPPOINT_HPP 2 | #define CONTOURKLIP_SWEEPPOINT_HPP 3 | 4 | #include 5 | #include "geometry_base.hpp" 6 | #include "bezier_utils.hpp" 7 | 8 | namespace contourklip { 9 | enum BooleanOpType { 10 | UNION = 0, 11 | INTERSECTION = 1, 12 | DIFFERENCE = 2, 13 | XOR = 3, 14 | DIVIDE = 4 15 | }; 16 | 17 | std::ostream &operator<<(std::ostream &o, const BooleanOpType &p) { 18 | switch (p) { 19 | case INTERSECTION: 20 | return o << "intersection"; 21 | case UNION: 22 | return o << "union"; 23 | case DIFFERENCE: 24 | return o << "difference"; 25 | case XOR: 26 | return o << "xor"; 27 | case DIVIDE: 28 | return o << "divide"; 29 | } 30 | return o; 31 | } 32 | 33 | namespace detail { 34 | enum EdgeType { 35 | NORMAL, SAME_TRANSITION, DIFFERENT_TRANSITION 36 | }; 37 | enum PolygonType { 38 | SUBJECT = 0, 39 | CLIPPING = 1 40 | }; 41 | 42 | struct SweepPoint { 43 | SweepPoint() = default; 44 | 45 | bool left = false; 46 | Point2d point; 47 | SweepPoint *other_point = nullptr; 48 | PolygonType ptype{}; 49 | std::size_t contourid = 0; 50 | 51 | //index at which SL iterator to other sp is stored 52 | std::size_t other_iter_pos = 0; 53 | bool in_out = false; // if for a ray passing upwards into the edge, it is an inside-outside transition 54 | bool other_in_out = false; // inout for the closest edge downward in SL that is from the other polygon 55 | 56 | // indicates if the edge associated to this point is in the result contour of the clipping 57 | // operation given by the index. 0 = default, 1 = INTERSECTION, 2 = DIFFERENCE 58 | std::array in_result{}; 59 | // follows the same principle, but instead maps to the previous SweepPoint* (downwards) 60 | // which is in the result. 61 | std::array prev_in_result{}; 62 | 63 | //the following fields are used when connecting the edges 64 | std::size_t pos = 0; // position in the result array 65 | bool result_in_out = false; //if the associated edge is an in out transition into its result contour 66 | std::size_t result_contour_id = 0; 67 | EdgeType edgetype = NORMAL; 68 | 69 | //used if segment is curve 70 | bool curve = false; 71 | bool islast = false; 72 | Point2d controlp; 73 | Point2d initial_controlp; 74 | SweepPoint *start = nullptr; 75 | 76 | SweepPoint(bool left, const Point2d &point, 77 | SweepPoint *otherPoint) : 78 | left(left), point(point), other_point(otherPoint) {} 79 | 80 | explicit SweepPoint(const Point2d &point) : 81 | point(point) {} 82 | 83 | bool vertical() const { return point.x() == other_point->point.x(); } 84 | 85 | void set_if_left() { 86 | left = increasing(point, other_point->point); 87 | this->other_point->left = !left; 88 | } 89 | }; 90 | 91 | std::ostream &operator<<(std::ostream &o, const SweepPoint &p) { 92 | if (p.other_point) { 93 | o << "[" << p.point << "->" << p.other_point->point; 94 | if (p.left && p.curve) o << "\n--->c" << p.controlp << "->" << p.other_point->controlp; 95 | o << ", l " << p.left 96 | << ", res " << p.in_result[0] 97 | << ", ptype " << p.ptype 98 | << ", cid " << p.result_contour_id 99 | << ", c " << p.curve 100 | << "]"; 101 | return o; 102 | } else { 103 | return o << "[" << p.point << "->" << " [nullptr] " << p.left << "]"; 104 | } 105 | } 106 | 107 | bool overlapping(const SweepPoint *e1, const SweepPoint *e2) { 108 | if (e1->curve != e2->curve) { 109 | return false; 110 | } 111 | bool overlapping_ends = e1->point == e2->point 112 | && e1->other_point->point == e2->other_point->point; 113 | if (!e1->curve) { 114 | return overlapping_ends; 115 | } 116 | 117 | return overlapping_ends 118 | && e1->controlp == e2->controlp 119 | && e1->other_point->controlp == e2->other_point->controlp; 120 | } 121 | 122 | //Returns true iff the segment associated with e1 is below a point p. 123 | auto curve_below_point = [](const SweepPoint *e1, const Point2d &p) -> bool { 124 | 125 | CubicBezier c{e1->point, e1->controlp, 126 | e1->other_point->controlp, e1->other_point->point}; 127 | 128 | double a = 0., b = 1.; 129 | Point2d left = c.p0; 130 | Point2d right = c.p3; 131 | Point2d sample; 132 | while ((b - a) > 1e-10) { 133 | if (left.y() < p.y() && right.y() < p.y()) { 134 | return true; 135 | } 136 | if (left.y() >= p.y() && right.y() >= p.y()) { 137 | return false; 138 | } 139 | double mid = 0.5 * (a + b); 140 | sample = beziermap(c, mid); 141 | if (sample.x() < p.x()) { 142 | a = mid; 143 | left = sample; 144 | } else { 145 | b = mid; 146 | right = sample; 147 | } 148 | } 149 | return sample.y() < p.y(); 150 | }; 151 | 152 | 153 | // check if the associated bezier curve of the first is below the associated bezier curve of the second. 154 | // preconditions: both are curves, share the endpoint (may be start or end), and otherwise do not intersect. 155 | template 156 | bool curve_below(const SweepPoint *e1, const SweepPoint *e2, CollinearF f = {}) { 157 | if (!f(e1->point, e1->controlp, e2->controlp) 158 | && e1->point != e1->controlp) { // special case with vanishing derivative 159 | if (vertical(e1->point, e1->controlp)) { 160 | return e1->controlp.y() < e1->point.y(); 161 | } 162 | return above_line(e1->point, e1->controlp, e2->controlp); 163 | } 164 | const SweepPoint *sp = e1; 165 | const SweepPoint *other = e2; 166 | bool reversed; 167 | // we want to sample the point on the curve which is shortest in the x direction 168 | if ((reversed = e1->left == (e1->other_point->point.x() 169 | > e2->other_point->point.x()))) { 170 | std::swap(sp, other); 171 | } 172 | Point2d a = beziermap(sp->point, sp->controlp, 173 | sp->other_point->controlp, sp->other_point->point, 0.5); 174 | 175 | CubicBezier tmp{other->point, other->controlp, 176 | other->other_point->controlp, other->other_point->point}; 177 | double t = t_from_x(tmp, a.x()); 178 | 179 | double y_other = beziermap(tmp, t).y(); 180 | return reversed ? a.y() > y_other : a.y() < y_other; 181 | } 182 | 183 | // This is the comparator used for the sweeppoint queue. returns true iff e1 < e2. 184 | template 185 | bool queue_comp(const SweepPoint *e1, const SweepPoint *e2) { 186 | CollinearF collinear{}; 187 | if (e1->point.x() != e2->point.x()) 188 | return e1->point.x() < e2->point.x(); 189 | if (e1->point.y() != 190 | e2->point.y()) 191 | return e1->point.y() < e2->point.y(); 192 | 193 | if (e1->left != e2->left) { 194 | //right endpoint is processed first. 195 | return e2->left; 196 | } 197 | 198 | if (overlapping(e1, e2)) { 199 | return e1->ptype < e2->ptype; 200 | } 201 | // Both events represent lines 202 | if (!e1->curve && !e2->curve) { 203 | if (!collinear(e1->point, e1->other_point->point, e2->other_point->point)) { 204 | // the event associate to the bottom segment is processed first 205 | return above_line(e1->point, e1->other_point->point, e2->other_point->point); 206 | } 207 | return e1->ptype < e2->ptype; 208 | } 209 | 210 | // very special case where curve_below fails to differentiate due to round-off. 211 | // In that case we use some consistent criterion. 212 | // At this point the segments do not exactly overlap. 213 | bool a = curve_below(e1, e2); 214 | bool b = curve_below(e2, e1); 215 | if (a == b) { 216 | if (e1->ptype != e2->ptype) return e1->ptype < e2->ptype; 217 | if (e1->other_point->point != e2->other_point->point) 218 | return increasing(e1->other_point->point, e2->other_point->point); 219 | if (e1->controlp != e2->controlp) return increasing(e1->controlp, e2->controlp); 220 | return increasing(e1->other_point->controlp, e2->other_point->controlp); 221 | } 222 | //at least one point is from a curve 223 | return a; 224 | } 225 | 226 | // Comparator used for the sweep line. returns true iff le1 < le2. 227 | // Note that only left events can be in the sweep line. 228 | template 229 | struct SComp { 230 | bool operator()(const SweepPoint *le1, const SweepPoint *le2) const { 231 | if (le1 == le2) 232 | return false; 233 | 234 | if (overlapping(le1, le2)) { 235 | return le1->ptype < le2->ptype; 236 | } 237 | 238 | if (le1->point == le2->point) { 239 | return queue_comp(le1, le2); 240 | } 241 | 242 | if (le1->point.x() == le2->point.x()) { 243 | return le1->point.y() < le2->point.y(); 244 | } 245 | 246 | if (!le1->curve && !le2->curve) { 247 | if (queue_comp(le1, le2)) { 248 | return above_line(le1->point, 249 | le1->other_point->point, le2->point); 250 | } 251 | return above_line(le2->point, 252 | le1->point, le2->other_point->point); 253 | } 254 | 255 | //one of the segments is a curve. 256 | if (queue_comp(le1, le2)) { 257 | // le1 has been inserted first. 258 | return curve_below_point(le1, le2->point); 259 | } 260 | return !curve_below_point(le2, le1->point); 261 | } 262 | }; 263 | } 264 | } 265 | #endif //CONTOURKLIP_SWEEPPOINT_HPP -------------------------------------------------------------------------------- /testcases/geometry/curves_nontrivial04/in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /include/svg_io.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONTOURKLIP_SVG_IO_HPP 2 | #define CONTOURKLIP_SVG_IO_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "geometry_base.hpp" 11 | 12 | /* 13 | * some functionality to instantiate contours from a svg file, and write back contours 14 | * to a svg file. It works for svg files which have the same structure as the 15 | * testcases, and it is therefore quite limited and not very robust. 16 | */ 17 | 18 | 19 | void path_to_str(const contourklip::Contour &c, std::string &out, bool close_path = true) { 20 | if (c.size() == 0) return; 21 | 22 | std::ostringstream os; 23 | os << std::setprecision(10); 24 | os << "M"; 25 | 26 | auto svgf = [&os](const contourklip::Point2d &p) { 27 | os << p.x() << ", " << p.y(); 28 | }; 29 | 30 | svgf(c.front_point()); 31 | 32 | for (std::size_t i = 1; i < c.size(); ++i) { 33 | if (c[i].segment_shape() == contourklip::LINE) { 34 | os << (i == 0 ? " " : " L"); 35 | svgf(c[i].point()); 36 | } else { 37 | os << (i == 0 ? " " : " C"); 38 | svgf(c[i].c1()); 39 | os << " "; 40 | svgf(c[i].c2()); 41 | os << " "; 42 | svgf(c[i].point()); 43 | 44 | } 45 | } 46 | if (close_path) os << "Z"; 47 | out += os.str(); 48 | } 49 | 50 | std::string path_to_str(const contourklip::Contour &c, bool close_path = true) { 51 | std::string out{}; 52 | path_to_str(c, out, close_path); 53 | return out; 54 | } 55 | 56 | void multipolygon_to_str(const std::vector &p, std::string &out) { 57 | for (const auto &c: p) { 58 | path_to_str(c, out); 59 | } 60 | } 61 | 62 | std::string multipolygon_to_str(const std::vector &p) { 63 | std::string out; 64 | multipolygon_to_str(p, out); 65 | return out; 66 | } 67 | 68 | std::string segment_to_path_str(const contourklip::Point2d &p0, const contourklip::Point2d p1) { 69 | 70 | contourklip::Contour p{p0, p1}; 71 | std::string out; 72 | path_to_str(p, out, false); 73 | return out; 74 | } 75 | 76 | std::string segment_to_path_str(const contourklip::detail::Segment &seg) { 77 | return segment_to_path_str(seg.first, seg.second); 78 | } 79 | 80 | std::string 81 | bezier_to_path_str(const contourklip::Point2d &p0, const contourklip::Point2d p1, const contourklip::Point2d p2, 82 | const contourklip::Point2d p3) { 83 | contourklip::Contour p{p0, p1, p2, p3}; 84 | std::string out; 85 | path_to_str(p, out, false); 86 | return out; 87 | } 88 | 89 | std::string bezier_to_path_str(const contourklip::detail::CubicBezier &c) { 90 | return bezier_to_path_str(c.p0, c.p1, c.p2, c.p3); 91 | } 92 | 93 | std::string bezier_to_path_str(const std::array &Bx, const std::array &By) { 94 | return bezier_to_path_str({Bx[0], By[0]}, {Bx[1], By[1]}, 95 | {Bx[2], By[2]}, {Bx[3], By[3]}); 96 | } 97 | 98 | void str_replace(std::string &str, const std::string &toreplace, const std::string &with) { 99 | auto length = toreplace.size(); 100 | auto length_new = with.size(); 101 | std::size_t index = 0; 102 | while ((index = str.find(toreplace, index)) != std::string::npos) { 103 | str.replace(index, length, with); 104 | index += length_new; 105 | } 106 | } 107 | 108 | bool path_from_svg_string(std::istringstream &path_str, contourklip::Contour &out, bool verbose = false) { 109 | 110 | if (path_str.get() != 'M') { 111 | return 0; 112 | } 113 | 114 | auto parsepoint = [&path_str]() -> contourklip::Point2d { 115 | double curr_x, curr_y; 116 | path_str >> curr_x; 117 | if (path_str.peek() == ',') { 118 | char t; 119 | path_str >> t; // consume the separator 120 | } 121 | path_str >> curr_y; 122 | 123 | return contourklip::Point2d{curr_x, curr_y}; 124 | }; 125 | contourklip::Point2d start = parsepoint(); 126 | if (verbose) std::cout << "starting point_: " << start << '\n'; 127 | 128 | out.push_back(start); 129 | 130 | while (path_str.peek() != EOF) { 131 | char curr; 132 | path_str >> curr; 133 | if (verbose) std::cout << "curr " << curr << '\n'; 134 | if (path_str.bad()) { 135 | return false; 136 | } 137 | switch (curr) { 138 | case 'Z': 139 | if (verbose) std::cout << "parse done\n"; 140 | return true; 141 | case 'L': { 142 | contourklip::Point2d p = parsepoint(); 143 | out.push_back(p); 144 | if (verbose) std::cout << "parsed L" << p << '\n'; 145 | } 146 | continue; 147 | case 'C': { 148 | contourklip::Point2d p1 = parsepoint(); 149 | contourklip::Point2d p2 = parsepoint(); 150 | contourklip::Point2d pLast = parsepoint(); 151 | out.push_back(p1, p2, pLast); 152 | if (verbose) std::cout << "parsed C " << p1 << " " << p2 << pLast << '\n'; 153 | } 154 | continue; 155 | case 'H': { 156 | double x_curr; 157 | path_str >> x_curr; 158 | contourklip::Point2d p1{x_curr, out.back_point().y()}; 159 | if (verbose) std::cout << "parsed H" << p1 << '\n'; 160 | out.push_back(p1); 161 | } 162 | continue; 163 | 164 | case 'V': { 165 | double y_curr; 166 | path_str >> y_curr; 167 | contourklip::Point2d p1{out.back_point().x(), y_curr}; 168 | if (verbose) std::cout << "parsed V" << p1 << '\n'; 169 | out.push_back(p1); 170 | } 171 | continue; 172 | default: 173 | if (verbose) std::cout << "bad character encountered"; 174 | return false; 175 | } 176 | } 177 | return true; 178 | } 179 | 180 | void multipolygon_from_str(const std::string &in, std::vector &out, bool verbose = false) { 181 | 182 | if (in.size() <= 1) { 183 | return; 184 | } 185 | std::string copy = in; 186 | std::transform(copy.begin(), copy.end(), copy.begin(), ::toupper); 187 | //we do this so that parsing with istringstream is much easier. Of course, this is not efficient. 188 | str_replace(copy, "C", " C"); 189 | str_replace(copy, "L", " L"); 190 | str_replace(copy, "Z", " Z"); 191 | str_replace(copy, "H", " H"); 192 | str_replace(copy, "V", " V"); 193 | 194 | if (verbose) std::cout << "complete: \n" << copy << "\n\n"; 195 | 196 | std::istringstream path_str(copy); 197 | 198 | while (path_str.peek() != EOF) { 199 | contourklip::Contour curr{}; 200 | if (path_from_svg_string(path_str, curr)) { 201 | out.push_back(curr); 202 | } 203 | } 204 | } 205 | 206 | contourklip::detail::CubicBezier bezier_from_str(const std::string &path) { 207 | std::vector poly{}; 208 | multipolygon_from_str(path, poly); 209 | assert(!poly.empty() && poly.at(0).size() > 1); 210 | auto seg = poly.at(0)[1]; 211 | assert(seg.segment_shape() == contourklip::CUBIC_BEZIER); 212 | return {poly.at(0).front_point(), seg.c1(), seg.c2(), seg.point()}; 213 | } 214 | 215 | class BasicPathLoader { 216 | std::string svg_in; 217 | public: 218 | explicit BasicPathLoader(const std::string &filepath) { 219 | std::ifstream ifs{filepath}; 220 | if (!ifs) { 221 | std::cout << "file opening unsuccessful. requested path:\n "; 222 | std::cout << filepath; 223 | std::cout << "\ncurrent path is\n "; 224 | std::cout << std::filesystem::current_path().string() << std::endl << std::flush; 225 | std::cout << '\n'; 226 | assert(false); 227 | } 228 | svg_in.assign((std::istreambuf_iterator(ifs)), 229 | (std::istreambuf_iterator())); 230 | 231 | preprocess(); 232 | parse_dims(); 233 | parse_paths(); 234 | } 235 | 236 | std::tuple get_dims() { 237 | return {start_x, start_y, width, height}; 238 | } 239 | 240 | std::vector paths; 241 | double start_x{}, start_y{}, width{}, height{}; 242 | 243 | private: 244 | void preprocess() { 245 | str_replace(svg_in, " ", " "); 246 | str_replace(svg_in, "\n", ""); 247 | } 248 | 249 | void parse_paths() { 250 | 251 | const std::regex path_regex{R"()"}; 252 | auto it = std::sregex_iterator(svg_in.begin(), svg_in.end(), path_regex); 253 | for (auto &match = it; match != std::sregex_iterator(); match++) { 254 | if (match->size() > 1) { 255 | paths.emplace_back(*(match->begin() + 1)); 256 | } 257 | } 258 | } 259 | 260 | void parse_dims() { 261 | const std::regex viewbox_regex{ 262 | R"(svg(?:[\s\S]*?)viewBox=\"((-?[\d.]+)? ([-?\d.]+)? ([-?\d.]+)? ([-?\d.]+)?)\"(?:[\s\S]*?))" 263 | }; 264 | auto m = std::sregex_iterator(svg_in.begin(), svg_in.end(), viewbox_regex); 265 | if (m->size() >= 5) { 266 | try { 267 | start_x = std::stoi(*(m->begin() + 2)); 268 | start_y = std::stoi(*(m->begin() + 3)); 269 | width = std::stoi(*(m->begin() + 4)); 270 | height = std::stoi(*(m->begin() + 5)); 271 | } 272 | catch (std::invalid_argument &) { 273 | std::cout << "could not parse height and width"; 274 | std::abort(); 275 | } 276 | } else { 277 | std::cout << "could not parse dimensions data"; 278 | std::abort(); 279 | } 280 | } 281 | }; 282 | 283 | class BasicPathWriter { 284 | public: 285 | 286 | std::string path_prefx; 287 | 288 | BasicPathWriter(double width, double height) : BasicPathWriter{0, 0, width, height} {} 289 | 290 | BasicPathWriter(double start_x, double start_y, double width, double height) : 291 | start_x(start_x), start_y(start_y), width(width), height(height) { 292 | out = std::ostringstream{}; 293 | put_prefix(); 294 | } 295 | 296 | explicit BasicPathWriter(const std::tuple &dims) : 297 | BasicPathWriter{std::get<0>(dims), std::get<1>(dims), 298 | std::get<2>(dims), std::get<3>(dims)} {} 299 | 300 | void push_path_str(const std::string &path, const std::string &fillcolor = "lightgrey", 301 | const std::string &stroke = "black", double factor = 1., double opacity = 0.6) { 302 | out << R"()"; 308 | out << '\n'; 309 | } 310 | 311 | void push_circle(const double cx, const double cy, const double r, const std::string &color) { 312 | out << ""; 314 | out << '\n'; 315 | } 316 | 317 | void push_circle(const double cx, const double cy) { 318 | push_circle(cx, cy, std::min(height, width) / 400, "black"); 319 | } 320 | 321 | void push_circle(const double cx, const double cy, const std::string &color) { 322 | push_circle(cx, cy, std::min(height, width) / 400, color); 323 | } 324 | 325 | void push_text(const contourklip::Point2d &p, const std::string &text, double r = 0.01) { 326 | out << R"()" << text << R"()"; 330 | } 331 | 332 | void write_to(const std::string &filename) { 333 | // std::cout << "writing to file, contents: " << out.rdbuf(); 334 | std::ofstream outfile{path_prefx + filename}; 335 | outfile << out.rdbuf()->str(); 336 | outfile << get_suffix(); 337 | } 338 | 339 | private: 340 | void put_prefix() { 341 | std::string t = R"()"; 342 | std::string t2 = R"()"; 344 | out << t << '\n' << t2; 345 | out << start_x << " " << start_y << " " << width << " " << height << t3 << '\n'; 346 | } 347 | 348 | static std::string get_suffix() { 349 | return ""; 350 | } 351 | 352 | double start_x, start_y, width, height; 353 | std::ostringstream out{}; 354 | }; 355 | 356 | #endif //CONTOURKLIP_SVG_IO_HPP -------------------------------------------------------------------------------- /test/test_utilities.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONTOURKLIP_TEST_UTILITIES_HPP 2 | #define CONTOURKLIP_TEST_UTILITIES_HPP 3 | 4 | #include "svg_io.hpp" 5 | #include "geometry_generation.hpp" 6 | #include "bezier_utils.hpp" 7 | #include "polyclip.hpp" 8 | 9 | #include "doctest.h" 10 | 11 | std::string TESTCASE_ROOT_DIR = "./../testcases"; 12 | std::string TESTCASE_DIR = std::string{TESTCASE_ROOT_DIR} + "/geometry" ; 13 | std::string TESTCASE_OTHERS_DIR = std::string{TESTCASE_ROOT_DIR} + "/various" ; 14 | constexpr double AREA_EPS = 1; 15 | 16 | void set_cout_precision(int prec = 10){ 17 | std::cout.setf(std::ios::unitbuf); 18 | std::cout.precision(prec); 19 | std::cout << std::fixed; 20 | } 21 | 22 | std::tuple load_testcase(const std::string& case_name, 23 | std::vector &a, 24 | std::vector &b){ 25 | std::string in_path = TESTCASE_DIR +"/" + case_name +"/in.svg"; 26 | BasicPathLoader pl(in_path); 27 | REQUIRE(pl.paths.size() >=2); 28 | multipolygon_from_str(pl.paths[0], a); 29 | multipolygon_from_str(pl.paths[1], b); 30 | return pl.get_dims(); 31 | } 32 | 33 | bool parametric_v(double t){ 34 | bool numeric= !std::isnan(t) && !std::isinf(t); 35 | bool tinrange =0<=t && t<=1; 36 | return numeric && tinrange; 37 | } 38 | 39 | void sweeppoint_init(const contourklip::Segment &seg, contourklip::detail::SweepPoint *a, contourklip::detail::SweepPoint *b) { 40 | 41 | if (contourklip::increasing(seg.first, seg.second)) { 42 | a->point = seg.first; 43 | b->point = seg.second; 44 | } else { 45 | b->point = seg.first; 46 | a->point = seg.second; 47 | } 48 | a->left = true; 49 | b->left = false; 50 | a->other_point = b; 51 | b->other_point = a; 52 | a->controlp = a->other_point->point; 53 | b->controlp = b->other_point->point; 54 | a->curve = b->curve = false; 55 | } 56 | 57 | void sweeppoint_init(const contourklip::CubicBezier &c, contourklip::detail::SweepPoint *a, contourklip::detail::SweepPoint *b) { 58 | 59 | if (contourklip::increasing(c.p0, c.p3)) { 60 | a->point = c.p0; 61 | b->point = c.p3; 62 | a->controlp = c.p1; 63 | b->controlp = c.p2; 64 | } else { 65 | a->point = c.p3; 66 | b->point = c.p0; 67 | b->controlp = c.p1; 68 | a->controlp = c.p2; 69 | } 70 | a->left = true; 71 | b->left = false; 72 | a->other_point = b; 73 | b->other_point = a; 74 | a->curve = b->curve = true; 75 | } 76 | 77 | std::tuple load_testcase(const std::string &case_name, contourklip::Contour &a, 78 | contourklip::Contour &b) { 79 | std::vector p1{}; 80 | std::vector p2{}; 81 | auto dims = load_testcase(case_name, p1, p2); 82 | REQUIRE(!p1.empty()); 83 | REQUIRE(!p2.empty()); 84 | a = p1.front(); 85 | b = p2.front(); 86 | return dims; 87 | } 88 | 89 | std::tuple load_contour(const std::string &in_path, contourklip::Contour &a) { 90 | 91 | std::vector p1{}; 92 | BasicPathLoader pl(in_path); 93 | multipolygon_from_str(pl.paths.front(), p1); 94 | REQUIRE(!p1.empty()); 95 | a = p1.front(); 96 | 97 | return pl.get_dims(); 98 | } 99 | 100 | void save_contour(const std::string &out_path, contourklip::Contour &a){ 101 | auto [min_x, min_y, max_x, max_y] = contourklip::contourbbox(a); 102 | BasicPathWriter w{min_x, min_y, (max_x-min_x), (max_y-min_y)}; 103 | w.push_path_str(multipolygon_to_str({a})); 104 | w.write_to(out_path); 105 | } 106 | 107 | void save_contours(const std::string &out_path, std::vector &a){ 108 | if(a.empty()) return; 109 | if(a.front().size() ==0 ) return; 110 | auto [min_x, min_y , max_x , max_y] = contourklip::contourbbox(a.front()); 111 | for (const auto &contour: a) { 112 | auto [cmin_x, cmin_y, cmax_x, cmax_y] = contourklip::contourbbox(contour); 113 | min_x = std::min(cmin_x, min_x); 114 | min_y = std::min(cmin_y, min_y); 115 | max_x = std::max(cmax_x, max_x); 116 | max_y = std::max(cmax_y, max_y); 117 | } 118 | double offset = 5; 119 | min_x -= offset; 120 | max_x += offset; 121 | min_y -= offset; 122 | max_y += offset; 123 | BasicPathWriter w{min_x, min_y, (max_x-min_x), (max_y-min_y)}; 124 | w.push_path_str(multipolygon_to_str(a)); 125 | w.write_to(out_path); 126 | } 127 | 128 | void save_output(const std::string &case_name, BasicPathWriter &out, const contourklip::BooleanOpType& op) { 129 | std::ostringstream out_path{}; 130 | out_path << TESTCASE_DIR << "/" << case_name << "/" << op << "_out.svg"; 131 | // std::string out_path = std::string{TESTCASE_DIR} + "/" + case_name + "/out.svg"; 132 | out.write_to(out_path.str()); 133 | } 134 | 135 | void reverse_contours(std::vector& poly){ 136 | for (auto &contour: poly) { 137 | contour.reverse(); 138 | } 139 | } 140 | 141 | int check_monotonic_split(const contourklip::CubicBezier& c){ 142 | 143 | auto horizontalorvertical 144 | = [](const contourklip::Point2d& a, const contourklip::Point2d& b){ 145 | return (std::abs(a.y()-b.y()) < 1e-8) || (std::abs(a.x()-b.x()) < 1e-8); 146 | }; 147 | 148 | double t =0; 149 | int num =0; 150 | auto dosplit = [&](double u, contourklip::Extremity_Direction d){ 151 | REQUIRE(u != t); 152 | auto maybesubsegment = contourklip::sub_bezier(c, t, u); 153 | REQUIRE(maybesubsegment); 154 | contourklip::CubicBezier subsegment = *maybesubsegment; 155 | bool tmp = horizontalorvertical(subsegment.p0, subsegment.p1) 156 | || horizontalorvertical(subsegment.p2, subsegment.p3); 157 | CHECK(tmp); 158 | t = u; 159 | num++; 160 | }; 161 | contourklip::bezier_monotonic_split(c, dosplit); 162 | CHECK(num <= 4); 163 | if(num >0){ 164 | dosplit(1, contourklip::X_Extremity); 165 | //n inner points lead to n+1 intervals 166 | num--; 167 | } 168 | return num; 169 | } 170 | 171 | int check_line_curve_inter(const contourklip::Segment& seg, const contourklip::CubicBezier& c){ 172 | int num =0; 173 | auto verify = [&seg , &c, &num](double t, double u, contourklip::Point2d p) { 174 | CHECK(parametric_v(t)); 175 | CHECK(parametric_v(u)); 176 | contourklip::Point2d interp = linear_map(seg, t); 177 | contourklip::Point2d interp2 = beziermap(c, u); 178 | CHECK(contourklip::detail::approx_equal(interp, interp2, 1e-4)); 179 | #ifdef DEBUG 180 | std::cout << "intersection at " << t << ", " << u << " with point " << p << '\n'; 181 | #endif 182 | num++; 183 | }; 184 | 185 | contourklip::line_bezier_inter(seg, c, verify); 186 | return num; 187 | } 188 | 189 | int check_curve_curve_inter(const contourklip::CubicBezier& a, const contourklip::CubicBezier& b){ 190 | 191 | int num = 0; 192 | std::vector inters{}; 193 | std::vector inters1{}; 194 | bool switched = false; 195 | 196 | #ifdef DEBUG 197 | std::cout.precision(20); 198 | std::cout << std::fixed; 199 | #endif 200 | auto verify = [&](double t, double u, contourklip::Point2d p) { 201 | CHECK(parametric_v(t)); 202 | CHECK(parametric_v(u)); 203 | if (switched) std::swap(t, u); 204 | contourklip::Point2d interp = beziermap(a, t); 205 | contourklip::Point2d interp2 = beziermap(b, u); 206 | #ifdef DEBUG 207 | std::cout << "intersection at " << t << " " << u << interp << " " << interp2 << '\n'; 208 | #endif 209 | CHECK(contourklip::detail::approx_equal(interp, interp2, 1e-4)); 210 | CHECK(contourklip::detail::approx_equal(interp, p, 1e-4)); 211 | inters.push_back(p); 212 | }; 213 | 214 | contourklip::curve_curve_inter(a, b, verify, (double) 1e-9); 215 | #ifdef DEBUG 216 | std::cout << "------------------------\n"; 217 | #endif 218 | CHECK(inters.size()<=9); 219 | num = (int)inters.size(); 220 | switched= true; 221 | inters1 = inters; 222 | inters.clear(); 223 | 224 | contourklip::curve_curve_inter(b, a, verify, (double) 1e-9); 225 | 226 | CHECK(inters.size()<=9); 227 | CHECK(inters1.size() == inters.size()); 228 | CHECK(inters1 == inters); 229 | #ifdef DEBUG 230 | for (int i = 0; i < inters.size(); ++i) { 231 | std::cout << inters1[i] << " "<< inters[i] << '\n'; 232 | } 233 | #endif 234 | 235 | return num; 236 | } 237 | 238 | 239 | bool area_check(const std::vector &a, const std::vector &b){ 240 | return (contourklip::detail::multipolygon_area(a) - contourklip::detail:: multipolygon_area(b)) < AREA_EPS; 241 | } 242 | 243 | bool area_check_inter(const std::vector &u_ab, const std::vector &xor_ab, const std::vector &i_ab){ 244 | 245 | double r1 = contourklip::detail::multipolygon_area(u_ab); 246 | double r2 = contourklip::detail::multipolygon_area(xor_ab); 247 | double r3 = contourklip::detail::multipolygon_area(i_ab); 248 | 249 | return std::abs(r1-(r2+r3)) < AREA_EPS; 250 | } 251 | 252 | 253 | bool area_check_diff(const std::vector &a_sub_b, const std::vector &b_sub_a, const std::vector &xor_ab){ 254 | 255 | double r1 = contourklip::detail::multipolygon_area(xor_ab); 256 | double r2 = contourklip::detail::multipolygon_area(a_sub_b); 257 | double r3 = contourklip::detail::multipolygon_area(b_sub_a); 258 | #ifdef DEBUG 259 | std::cout << "xor, a-b, b-a areas: " << r1 << ", " << r2 << ", " << r3 << '\n'; 260 | #endif 261 | return std::abs(r1 - (r2+r3)) < AREA_EPS; 262 | } 263 | 264 | void test_op_unary(const std::vector &a){ 265 | std::vector a_inter_a{}; 266 | std::vector a_union_a{}; 267 | std::vector a_diff_a{}; 268 | std::vector a_xor_a{}; 269 | 270 | std::vector a_union_none{}; 271 | std::vector none_union_a{}; 272 | std::vector a_inter_none{}; 273 | std::vector none_inter_a{}; 274 | 275 | #ifdef DEBUG 276 | std::cout << "testing a inter a\n"; 277 | #endif 278 | CHECK(contourklip::clip(a, a, a_inter_a, contourklip::INTERSECTION)); 279 | #ifdef DEBUG 280 | std::cout << "testing a union a\n"; 281 | 282 | #endif 283 | CHECK(contourklip::clip(a, a, a_union_a, contourklip::UNION)); 284 | 285 | #ifdef DEBUG 286 | std::cout << "testing a inter none\n"; 287 | #endif 288 | CHECK(contourklip::clip(a, {}, a_inter_none, contourklip::INTERSECTION)); 289 | #ifdef DEBUG 290 | std::cout << "testing none union a\n"; 291 | #endif 292 | CHECK(contourklip::clip({}, a, none_union_a, contourklip::INTERSECTION)); 293 | #ifdef DEBUG 294 | std::cout << "testing a union none\n"; 295 | #endif 296 | CHECK(contourklip::clip(a, {}, a_union_none, contourklip::UNION)); 297 | 298 | } 299 | 300 | auto test_op_binary(const std::vector &a, const std::vector &b, 301 | bool check_area = true){ 302 | std::vector a_inter_b{}; 303 | std::vector a_union_b{}; 304 | std::vector b_inter_a{}; 305 | std::vector b_union_a{}; 306 | std::vector a_sub_b{}; 307 | std::vector b_sub_a{}; 308 | std::vector a_xor_b{}; 309 | std::vector b_xor_a{}; 310 | 311 | std::vector a_dissolve_b{}; 312 | std::vector b_dissolve_a{}; 313 | 314 | #ifdef DEBUG 315 | std::cout << "testing intersection a b\n"; 316 | #endif 317 | CHECK(contourklip::clip(a, b, a_inter_b, contourklip::INTERSECTION)); 318 | #ifdef DEBUG 319 | std::cout << "testing intersection b a\n"; 320 | #endif 321 | CHECK(contourklip::clip(b, a, b_inter_a, contourklip::INTERSECTION)); 322 | #ifdef DEBUG 323 | std::cout << "testing union a b\n"; 324 | #endif 325 | CHECK(contourklip::clip(a, b, a_union_b, contourklip::UNION)); 326 | #ifdef DEBUG 327 | std::cout << "testing union b a\n"; 328 | #endif 329 | CHECK(contourklip::clip(b, a, b_union_a, contourklip::UNION)); 330 | #ifdef DEBUG 331 | std::cout << "testing difference a b\n"; 332 | #endif 333 | CHECK(contourklip::clip(a, b, a_sub_b, contourklip::DIFFERENCE)); 334 | #ifdef DEBUG 335 | std::cout << "testing difference b a\n"; 336 | #endif 337 | CHECK(contourklip::clip(b, a, b_sub_a, contourklip::DIFFERENCE)); 338 | #ifdef DEBUG 339 | std::cout << "testing xor a b\n"; 340 | #endif 341 | CHECK(contourklip::clip(a, b, a_xor_b, contourklip::XOR)); 342 | #ifdef DEBUG 343 | std::cout << "testing xor b a\n"; 344 | #endif 345 | CHECK(contourklip::clip(b, a, b_xor_a, contourklip::XOR)); 346 | 347 | #ifdef DEBUG 348 | std::cout << "testing dissolve a, b\n"; 349 | #endif 350 | CHECK(contourklip::clip(a, b, a_dissolve_b, contourklip::DIVIDE)); 351 | #ifdef DEBUG 352 | std::cout << "testing dissolve b, a\n"; 353 | #endif 354 | CHECK(contourklip::clip(b, a, b_dissolve_a, contourklip::DIVIDE)); 355 | 356 | if(check_area) { 357 | CHECK(area_check(a_inter_b, b_inter_a)); 358 | CHECK(area_check(a_union_b, b_union_a)); 359 | CHECK(area_check(a_xor_b, b_xor_a)); 360 | CHECK(area_check(a_dissolve_b, b_dissolve_a)); 361 | 362 | CHECK(area_check_inter(a_union_b, a_xor_b, a_inter_b)); 363 | CHECK(area_check_diff(a_sub_b, b_sub_a, a_xor_b)); 364 | } 365 | 366 | return std::array{contourklip::detail::multipolygon_area(a_inter_b), 367 | contourklip::detail::multipolygon_area(a_union_b), 368 | contourklip::detail::multipolygon_area(a_sub_b), 369 | contourklip::detail::multipolygon_area(b_sub_a)}; 370 | } 371 | 372 | void test_ops(const std::vector &a, const std::vector &b, 373 | bool check_area = true){ 374 | std::vector a_rev = a; 375 | std::vector b_rev = b; 376 | reverse_contours(a_rev); 377 | reverse_contours(b_rev); 378 | 379 | test_op_unary(a); 380 | test_op_unary(a_rev); 381 | test_op_unary(b); 382 | test_op_unary(b_rev); 383 | 384 | test_op_binary(a, b, check_area); 385 | test_op_binary(a, b_rev, check_area); 386 | test_op_binary(a_rev, b, check_area); 387 | test_op_binary(a_rev, b_rev, check_area); 388 | test_op_binary(a, a_rev, check_area); 389 | test_op_binary(b, b_rev, check_area); 390 | } 391 | 392 | auto do_test = [](const std::string& tcase){ 393 | std::vector a; 394 | std::vector b; 395 | load_testcase(tcase, a, b); 396 | test_ops(a, b); 397 | }; 398 | #endif //CONTOURKLIP_TEST_UTILITIES_HPP -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ![Alt text](images/title.png) 6 | 7 | Contourklip is a self-contained single-header C++ library for boolean operations on multipolygons/contours ("polygon clipping"), where contours (paths) can consist of line segments *and* cubic bezier curves. 8 | 9 | The library focuses on correctness and a straightforward API. It is a modification and extension of the algorithm from the paper by Martínez[^1]. It supports the 5 common boolean operations, namely *union, intersection, difference, xor (symmetric difference), divide*. Please note that this library is in a relatively early state. 10 | 11 | 12 | 13 | ------ 14 | 15 | 16 | 17 | ## Installation 18 | 19 | Contourklip needs no dependencies and consists of a single header, simply add the header in `single_include` to your project. 20 | 21 | ## Usage 22 | 23 | This section is currently considered to be the documentation. Here is a basic example to get started, which corresponds to the computation shown in the title picture: 24 | 25 | ```c++ 26 | #include 27 | #include "contourklip.hpp" 28 | 29 | int main() { 30 | contourklip::Contour contour1{{0, 100}}; 31 | contour1.push_back({50, 100}); 32 | contour1.push_back({77.5, 100}, {100, 77.5}, {100, 50}); 33 | contour1.push_back({100, 22.5}, {77.5, 0}, {50, 0}); 34 | contour1.push_back({0, 0}); 35 | contour1.close(); 36 | 37 | contourklip::Contour contour2{{150, 25}}; 38 | contour2.push_back({100, 25}); 39 | contour2.push_back({72.3, 25}, {50, 47.3}, {50, 75}); 40 | contour2.push_back({50, 102.5}, {72.3, 125}, {100, 125}); 41 | contour2.push_back({150, 125}); 42 | contour2.close(); 43 | 44 | std::vector shape1{contour1}; 45 | std::vector shape2{contour2}; 46 | std::vector result{}; 47 | 48 | if(contourklip::clip(shape1, shape2, result, contourklip::INTERSECTION)){ 49 | std::cout << "clipping operation succeeded\n"; 50 | } 51 | 52 | for (const auto &contour: result) { 53 | std::cout << "contour:\n"; 54 | std::cout << contour; 55 | } 56 | return 0; 57 | } 58 | ``` 59 | 60 | Output: 61 | 62 | ``` 63 | clipping operation succeeded 64 | contour: 65 | (50, 75) 66 | (50, 49.5052) (68.8907, 28.5849) (93.4955, 25.4155) 67 | (97.6328, 32.6861) (100, 41.08) (100, 50) 68 | (100, 75.312) (80.9379, 96.388) (56.4602, 99.5815) 69 | (52.3456, 92.3116) (50, 83.9187) (50, 75) 70 | ``` 71 | 72 | In this case there is 1 contour in the result, and one can see that it indeed makes sense compared to the expected result (namely a "leaf" shape). 73 | 74 | 75 | Let's take a closer look: 76 | 77 | - Here, a multipolygon is indeed simply a vector of `Contour`s. 78 | 79 | - To compute a boolean operation we just have to call the following function, which looks quite intuitive: 80 | 81 | ```c++ 82 | bool clip(const std::vector &a, const std::vector &b, 83 | std::vector &result, BooleanOpType clippingop) 84 | ``` 85 | 86 | - The `BooleanOpType` should be one of `UNION, INTERSECTION, DIFFERENCE, XOR, DIVIDE`, as one would expect. 87 | 88 | - The function will store the result in `out`, and returns if the clipping operation succeeded. The function will only append to the result, and not modify or remove contours which are already in the result vector. 89 | 90 | - If one has mutipolygons which contain only 1 contour anyway, there's a corresponding overload for convenience: 91 | 92 | ```c++ 93 | bool clip(const Contour &a, const Contour &b, 94 | std::vector &result, BooleanOpType clippingop) 95 | ``` 96 | 97 | Note that the result remains a vector of contours. Consider for example the union of two nonintersecting polygons. 98 | 99 | IMPORTANT: at this point it is worth mentioning that if you are getting unexpected output, make sure to read the "assumptions and limitations" section carefully. 100 | 101 | ##### Contours & ContourComponents 102 | 103 | A `Contour` is a random-access container which represents a contour, by containing `ContourComponent`s. 104 | 105 | A `ContourComponent` represents a line segment which is connected to the previous point (1 point is needed), or a cubic bezier which is connected to the previous point (3 points are needed). So, the `ContourComponent` has 3 points and whether the `ContourComponent` is one or the other is determined by an `enum ComponentType {LINE = 0, CUBIC_BEZIER = 1}` . (Indeed there's no subclassing for line and bezier). If it is a line, the other 2 points are not meaningful. 106 | 107 | The following methods are provided: 108 | 109 | - `explicit ContourComponent(const Point2d &pLast)` initializes it so that it represents a line 110 | - `ContourComponent(const Point2d &c_1, const Point2d &c_2, const Point2d &p)` initializes it so that it represents a cubic bezier. 111 | - `ComponentType segment_shape() const` returns the enum associated with this instance (`LINE` or `CUBIC_BEZIER`) 112 | - `Point2d point() const `, `Point2d &point()` returns endpoint/ last point of the implied segment when connected to a previous point (in a contour). 113 | - `Point2d c1() const`, `Point2d& c1()` returns the first bezier control point. Meaningless if `this->segment_shape() == LINE`. 114 | - `Point2d c2() const`, `Point2d& c2()` returns the second bezier control point. Meaningless if `this->segment_shape() == LINE`. 115 | 116 | While `ContourComponent`s need not be used to initialize contours, they are useful to work with the result. 117 | 118 | Then, `Contour` provides the following methods: 119 | 120 | - `explicit Contour(const Point2d &start)` Constructor with starting point. This is what should be used by default. 121 | 122 | - `Contour(const Point2d &p0, const Point2d &c1)` initializes a contour from a line segment, for convenience. 123 | 124 | - `Contour(const Point2d &p0, const Point2d &c1, const Point2d &c2, const Point2d &p3) ` Initializes a contour from a cubic bezier, for convenience. 125 | 126 | - `Contour()` trivial constructor. Note that this can lead to degenerate contours if then the first component pushed back represents a cubic bezier, because then the starting point is missing. Therefore the first construct with a point is preferrable. 127 | 128 | - `void push_back(const Point2d &p)` appends a point, which creates a line segment with the previous point. 129 | 130 | - `void push_back(const Point2d &c2, const Point2d &p3, const Point2d &p)` appends a cubic bezier which is connected to the previous point. 131 | 132 | - `void push_back(const ContourComponent &start)` appends a `ContourComponent `. It is preferable to use the other methods. 133 | 134 | - `ContourComponent operator[](const size_t idx) const` , `ContourComponent& operator[](const size_t idx)` : returns the component at index idx. 135 | 136 | - `std::size_t size() const` returns the number of `ContourComponent`s. 137 | 138 | - `Point2d front_point() const` returns the starting point. 139 | 140 | - `Point2d back_point() const` returns the last segment point. 141 | 142 | - `ContourComponent &front()`, `ContourComponent front() const`: returns the first `ContourComponent` 143 | 144 | - `ContourComponent &back()`, `ContourComponent back() const`: returns the last `ContourComponent` 145 | 146 | - `bool is_closed() const` Returns true iff contour is closed, that is, the last segment point corresponds to the starting point. 147 | 148 | - `void close()` Closes the contour if not closed. 149 | 150 | - `void reverse()` Reverses the contour direction in-place. 151 | 152 | - `template void forward_segments(Consumer &out) const` pass all line segments or all cubic beziers (according to `ComponentType`) to a consumer callback `Consumer`, by passing 2 points for a line, or 4 point for a cubic bezier. The following will print all cubic beziers of a contour: 153 | 154 | ```c++ 155 | contourklip::Contour c1{{0, 0}}; 156 | c1.push_back({1, 0}); 157 | c1.push_back({2, 0}, {2, 1}, {1, 1}); 158 | c1.push_back({0, 1}); 159 | c1.close(); 160 | auto print_lines = [](const Point2d& a, const Point2d& b){ 161 | std::cout << "line: " << a << " " << b << "\n"; 162 | }; 163 | auto print_curves = [](const Point2d& p0, const Point2d& c1, const Point2d& c2, const Point2d& p3){ 164 | std::cout << "bezier: " << p0 << " " << c1 << " " << c2 << " " << p3 << "\n"; 165 | }; 166 | c1.forward_segments(print_lines); 167 | c1.forward_segments(print_curves); 168 | ``` 169 | 170 | Output: 171 | 172 | ``` 173 | line: (0, 0) (1, 0) 174 | line: (1, 1) (0, 1) 175 | line: (0, 1) (0, 0) 176 | bezier: (1, 0) (2, 0) (2, 1) (1, 1) 177 | ``` 178 | 179 | 180 | 181 | - `auto begin() const`, `auto begin()`, `auto end()`, `const auto end()` : iterator pairs, iterating over the `ContourComponent`s. 182 | 183 | Summing up, the following can be used to actually retrieve the points: 184 | 185 | ```c++ 186 | for (const auto &contour: result) { 187 | for (const auto &seg: contour) { 188 | switch (seg.segment_shape()) { 189 | case contourklip::LINE: 190 | //process line, extract point coordinates 191 | std::cout << "(" << seg.point().x() << ", " < 212 | class PolyClip { 213 | PolyClip(const std::vector &a, const std::vector &b, 214 | std::vector &result, BooleanOpType clippingop, Config c = {}); 215 | bool success(); 216 | void compute(); 217 | } 218 | ``` 219 | 220 | The workflow is thus as follows: we initialize an instance with appropriate parameters, call `compute()` and then check `success()`. 221 | 222 | There are 2 template parameters for geometry predicates: `Orient2dFunc`, to check if a point is on the left of a directed segment defined by 2 other points, and `CollinearFunc`, to check if 3 points are collinear. These default to an implementation which might not be robust with respect to roundoff errors in unlikely degenerate cases. 223 | 224 | Additionally, an optional `contourklip::Config` struct can be passed. It has the following fields: 225 | 226 | - `postprocess`: whether to perform any post-processing on a result contour. Concretely this means to split it into simpler contours if possible (i.e. if there are overlapping points) , and to remove points which are not needed (see next field). Defaults to `true`. 227 | - `remove_collinear`: wether to remove successive collinear points in the result contours. Defaults to `true`. Implied to be false when post-process is false. 228 | - `fail_on_approx_equal`: whether to set success to false if approximately equal points are detected. This is motivated by the fact that it likely indicates a numerical error. Defaults to `true`. 229 | - `approx_equal_tol`: the absolute tolerance to decide if 2 points are approximately equal. Defaults to `1e-8`. 230 | 231 | We illustrate this with the following example, which in this case ultimately performs the exact same operation as in the first example. Note that one needs c++>= 20 to allow default-constructible non-capturing lambdas. Otherwise one needs a functor, i.e. a struct with a `bool operator()` overload. 232 | 233 | ```c++ 234 | #include 235 | #include "contourklip.hpp" 236 | 237 | int main() { 238 | 239 | // ...contour initialization same as before 240 | 241 | std::vector shape1{contour1}; 242 | std::vector shape2{contour2}; 243 | std::vector result{}; 244 | 245 | //callback for determining if a is on the left of the segment p0, p1. 246 | // here it just calls the default callback. 247 | auto above = 248 | [](const contourklip::Point2d &p0, 249 | const contourklip::Point2d &p1, const contourklip::Point2d &a) -> bool{ 250 | return contourklip::detail::LeftOfLine{}(p0, p1, a); 251 | }; 252 | 253 | // callback for determining if 3 points are collinear. 254 | // Again this just calls the default implementation. 255 | auto collinear = 256 | [](const contourklip::Point2d &p0, 257 | const contourklip::Point2d &p1, const contourklip::Point2d &p2) -> bool{ 258 | return contourklip::detail::IsCollinear{}(p0, p1, p2); 259 | }; 260 | 261 | contourklip::Config c; 262 | c.postprocess = false; 263 | 264 | contourklip::PolyClip clip{shape1, shape2, result, 265 | contourklip::INTERSECTION, c}; 266 | clip.compute(); 267 | 268 | if(clip.success()){ 269 | std::cout << "clipping operation succeeded\n"; 270 | } 271 | return 0; 272 | } 273 | ``` 274 | 275 | 276 | 277 | ## Assumptions and Limitations 278 | 279 | ##### Definitions 280 | 281 | - A segment is either a line segment or a cubic bezier, and in the second case we also say bezier segment. 282 | 283 | - A contour is an ordered list of connected segments. 284 | 285 | - A multipolygon is a set of contours which defines a shape. While it makes sense to think of it as an svg path, it is not quite accurate since an svg path has more features. Also note that the term "polygon" is not ideal since that implies there are only lines, but "multipolygon" is more common than "multicontour". 286 | 287 | ##### Input contour treatment 288 | 289 | The algorithm always uses the "evenodd" fill rule when interpreting a multipolygon as a shape (see [here](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule)). One way to think about this is that all closed shapes that appear in the (same) multipolygon are XORed together. Hence, the contour direction (clockwise/counterclockwise) of any input contour is *not* taken into account. 290 | 291 | The algorithm also assumes each contour is closed, since clipping open contours is not well defined. 292 | 293 | ##### Shape structure of the output 294 | 295 | Although the visual output of a clipping operation is well-defined, sometimes there are multiple ways the output shape can be represented with contours. In general, no assumption is made about the output contour representation, in particular with respect to contour direction. However, an attempt is made to "disentangle" the output by post-processing it. Also note that for two visually identical inputs represented differently in terms of contours, the output representation may not be the same. 296 | 297 | A particular focus was put on the property that the output has clean curve geometry. This means that the output only has as much beziers as needed. 298 | 299 | ##### Preconditions 300 | 301 | The library makes certain assumptions about the input contours. The following may lead to an unsuccessful computation: 302 | 303 | - Two segments (line or bezier) of the *same* contour overlap in more than just one point (i.e. they share a subsegment) 304 | - A line segment has the same startpoint and endpoint 305 | - A bezier segment intersects itself (this might eventually be supported) 306 | - A bezier segment has the same startpoint and endpoint (this is a special case of the previous point) 307 | - two bezier segments share a point of tangency (i.e. they have a common point but without intersecting) which does not corrrespond to a start/endpoint. 308 | - A startpoint or endpoint (of a curve or line segment) is *on* another bezier curve B, unless it's on the startpoint or endpoint of the curve B. 309 | - Input data contains NaN or Inf values. 310 | 311 | Otherwise there are no restrictions, and in particular a contour can intersect itself. 312 | 313 | However, while the algorithm then *should* compute the correct result, it is best to still check wether it succeeded by checking `success()`, in particular because of numerical errors. 314 | 315 | ##### Circles, quadratic beziers and other curves 316 | 317 | The only curve type that is supported is the cubic bezier curve. As such, not all svg paths can be implemented in a `contourklip::Contour`. However note that quadratic beziers can be trivially elevated to cubic beziers, and circles or ellipses are often approximated with cubic beziers (indeed the approximation is very accurate). 318 | 319 | ##### A note about divide 320 | 321 | The *divide* operation can be thought of as retrieving all closed shapes that appear when looking at all overlapping contour segments. Here it is defined as the concatenation of *xor* and *intersection*. Note that this is not quite equivalent. 322 | 323 | ##### Numerical considerations 324 | 325 | - Clearly, the library actually computes an accurate numerical approximation of a clipping operation, not least because the intersection of beziers has no closed-form solution. After all the purpose of this library is to have an explicit shape representation of the clipping operation result. 326 | - The implementation might not be robust, but numerical issues leading to an incorrect state should be detected in which case `success()` returns false. Also, since contourklip is self-contained, it does not use a robust geometry kernel such as CGAL. 327 | 328 | ## About the algorithm 329 | 330 | Any clipping algorithm has to do the following: 331 | 332 | - calculate all segment intersections 333 | - determine which (sub)segments belong to the result 334 | - construct the result 335 | 336 | How to do it properly is nontrivial. The library uses the code/main idea from the paper by Martínez[^1], whose implementation is available on his website[^2]. Note that the paper is not concerned with beziers but "only" standard line polygons. The code is in the public domain and a few parts have been refactored into the library (with additional permission from the author). Other than that there are some significant differences, for example the original algorithm also computes the intersections during the plane sweep, which leads to very complicated (but more efficient) code. Instead we compute all pairwise intersections. Last but not least, support for cubic beziers has been added. 337 | 338 | To compute intersections of cubic bezier curves and generally do bezier-related operations, techniques from here[^3] have been used. 339 | 340 | Those working with very large *line* polygon data definitely may want to look at the original paper code instead (or use another library of course, such as Boost.Geometry). 341 | 342 | 343 | 344 | [^1]: https://doi.org/10.1016/j.advengsoft.2013.04.004 345 | [^2]: https://www4.ujaen.es/~fmartin/bop12.zip 346 | [^3]: https://scholarsarchive.byu.edu/cgi/viewcontent.cgi?article=1000&context=facpub 347 | -------------------------------------------------------------------------------- /test/unit.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | 3 | #include 4 | 5 | #include "doctest.h" 6 | 7 | #include "polyclip.hpp" 8 | #include "svg_io.hpp" 9 | #include "test_utilities.hpp" 10 | #include "bezier_utils.hpp" 11 | 12 | using namespace contourklip; 13 | 14 | TEST_CASE("contour segments"){ 15 | contourklip::Contour c1{{0, 0}}; 16 | c1.push_back({1, 0}); 17 | c1.push_back({2, 0}, {2, 1}, {1, 1}); 18 | c1.push_back({0, 1}); 19 | c1.close(); 20 | int num_lines=0, num_curves= 0; 21 | 22 | auto lines = [&](const Point2d& a, const Point2d& b){ 23 | CHECK(a != b); 24 | num_lines++; 25 | }; 26 | auto curves = [&](const Point2d& p0, const Point2d& p1, const Point2d& p2, const Point2d& p3){ 27 | CHECK(p0 != p1); 28 | num_curves++; 29 | }; 30 | c1.forward_segments(lines); 31 | c1.forward_segments(curves); 32 | CHECK(num_lines == 3); 33 | CHECK(num_curves == 1); 34 | } 35 | 36 | TEST_SUITE("linear intersections"){ 37 | 38 | bool check_linear_inter(const Segment &a, const Segment &b) { 39 | 40 | if(!intersect_segments(a, b)){ 41 | return false; 42 | } 43 | 44 | Segment a1 = {a.second, a.first}; 45 | Segment b1 = {b.second, b.first}; 46 | 47 | std::vector> cases{ 48 | {a, b}, 49 | {b, a}, 50 | {a1, b1}, 51 | {b1, a1}, 52 | {a1, b}, 53 | {b, a1}, 54 | {a, b1}, 55 | {b1, a} 56 | }; 57 | 58 | std::vector> result{}; 59 | for (const auto &seg_pair: cases) { 60 | result.push_back(intersect_segments(seg_pair.first, seg_pair.second)); 61 | REQUIRE(result.back()); 62 | } 63 | 64 | Point2d p = result.front()->p; 65 | for (std::size_t i = 0; i < result.size(); ++i) { 66 | CHECK(result[i]->p == p); 67 | auto p1 = linear_map(cases[i].first, result[i]->t1); 68 | auto p2 = linear_map(cases[i].second, result[i]->t2); 69 | CHECK(approx_equal(p1, p, 1e-5)); 70 | CHECK(approx_equal(p2, p, 1e-5)); 71 | } 72 | return true; 73 | } 74 | 75 | TEST_CASE("linear intersection commutativity") 76 | { 77 | Segment a{{54.4, 288.5}, 78 | {500, 509.5}}; 79 | Segment b{{500, 288.5}, 80 | {104.4, 500}}; 81 | 82 | CHECK(check_linear_inter(a, b)); 83 | } 84 | 85 | TEST_CASE("linear intersection commutativity 2") 86 | { 87 | 88 | Segment a{{1, 1}, {4, 1}}; 89 | Segment b{{2, 1}, {3, 4}}; 90 | 91 | CHECK(check_linear_inter(a, b)); 92 | } 93 | 94 | TEST_CASE("linear intersection on segment"){ 95 | 96 | Segment a{{1, 1}, {5, 1}}; 97 | Segment b{{0, 5}, {3, 1}}; 98 | Segment c{{8, 2}, {3, 1}}; 99 | 100 | SegInter u1 = *intersect_segments(a, b); 101 | SegInter u2 = *intersect_segments(a, c); 102 | 103 | CHECK(u1.p == u2.p); 104 | } 105 | } 106 | 107 | TEST_SUITE("splitting of beziers") { 108 | TEST_CASE("test casteljau subcurve split 2") { 109 | CubicBezier c{ 110 | {2089.13, 1077.93}, 111 | {1720.02, 1453.7}, 112 | {1720.02, 1453.7}, 113 | {3411.43, 855.997} 114 | }; 115 | CubicBezier subcurve = *sub_bezier(c, 0.1, 0.25); 116 | CHECK(beziermap(c, 0.1) == subcurve.p0); 117 | CHECK(beziermap(c, 0.25) == subcurve.p3); 118 | } 119 | 120 | TEST_CASE("test bezier monotonic 4 points") { 121 | 122 | CubicBezier c{{5, -8}, 123 | {1, -15}, 124 | {12, -5}, 125 | {11, -8}}; 126 | int num = check_monotonic_split(c); 127 | CHECK(num == 4); 128 | } 129 | 130 | TEST_CASE("test bezier monotonic split 3 points") { 131 | 132 | CubicBezier c{{5, -8}, 133 | {1, -12}, 134 | {12, -14}, 135 | {11, -5}}; 136 | int num = check_monotonic_split(c); 137 | CHECK(num == 3); 138 | } 139 | 140 | TEST_CASE("test monotonic bezier already monotonic") { 141 | 142 | CubicBezier c{{1845.298655314596090, 1408.195851834146197}, 143 | {1769.666501760373194, 1436.156138345120098}, 144 | {1720.019999999999982, 1453.700000000000045}, 145 | {1720.019999999999982, 1453.700000000000045}}; 146 | 147 | int num = check_monotonic_split(c); 148 | CHECK(num ==0); 149 | } 150 | 151 | TEST_CASE("test monotonic bezier with overlapping points") { 152 | 153 | CubicBezier c{ 154 | {1037.349999999999909, 169.037000000000006}, 155 | {1037.349999999999909, 169.037000000000006}, 156 | {865.234133861356099, 313.209452692659681}, 157 | {967.979899428897852, 412.444395647281397}}; 158 | 159 | int num = check_monotonic_split(c); 160 | CHECK(num ==1); 161 | } 162 | 163 | TEST_CASE("bezier monotonic split 1 point"){ 164 | 165 | CubicBezier c{{2517.160194232299091, 1028.619576515788594}, 166 | {2485.342247547211628, 1028.020143901298070}, 167 | {2443.735516136445767, 1030.236455866035385}, 168 | {2392.340000000000600, 1035.268512410000312}}; 169 | 170 | int num = check_monotonic_split(c); 171 | CHECK(num == 1); 172 | } 173 | 174 | TEST_CASE("bezier monotonic split close extrema"){ 175 | CubicBezier c {{2.52351, -2.82288}, {2.52393, -1.92694}, 176 | {2.44519, -0.6975}, {2.30787, 0.791758}}; 177 | 178 | int num = check_monotonic_split(c); 179 | CHECK(num == 1); 180 | } 181 | } 182 | 183 | TEST_SUITE("line-curve intersection") { 184 | TEST_CASE("line curve intersections basic") { 185 | Segment seg{{1, 2}, 186 | {5, 3}}; 187 | CubicBezier c{{1, 5}, 188 | {3, -2}, 189 | {4, -5}, 190 | {5, 8}}; 191 | 192 | int num = check_line_curve_inter(seg, c); 193 | CHECK(num == 2); 194 | } 195 | 196 | TEST_CASE("line curve intersections aligned") { 197 | Segment seg{{1, 0}, 198 | {1, 5}}; 199 | CubicBezier c{{1, 1}, 200 | {-3, 2}, 201 | {5, 3}, 202 | {1, 4}}; 203 | 204 | int num = check_line_curve_inter(seg, c); 205 | CHECK(num == 3); 206 | } 207 | 208 | TEST_CASE("line curve intersections connected2") { 209 | Segment seg{{1, 0}, 210 | {9, 4}}; 211 | CubicBezier c{{5.1, 2.05}, 212 | {-3, 2}, 213 | {12, -3}, 214 | {10, -5}}; 215 | 216 | int num = check_line_curve_inter(seg, c); 217 | CHECK(num == 2); 218 | } 219 | 220 | TEST_CASE("line curve intersections vertical connected") { 221 | Segment seg{{1, 0}, 222 | {1, 8}}; 223 | CubicBezier c{{1, 0}, 224 | {-4, 3}, 225 | {1.7, 4}, 226 | {1, 8}}; 227 | int num = check_line_curve_inter(seg, c); 228 | CHECK(num == 1); 229 | } 230 | 231 | TEST_CASE("line curve intersections vertical") { 232 | Segment seg{{2392.34, 917.237}, 233 | {2392.34, 1077.93}}; 234 | CubicBezier c{{2089.13, 1077.93}, 235 | {3411.43, 855.997}, 236 | {1720.02, 1453.7}, 237 | {1720.02, 1453.7}}; 238 | int num = check_line_curve_inter(seg, c); 239 | CHECK(num ==1); 240 | } 241 | 242 | TEST_CASE("line curve intersections vertical 2"){ 243 | 244 | CubicBezier c{{410.707,862.071}, {330.302,862.071},{265.023,796.792}, {265.023,716.388}}; 245 | Segment seg{{265.023,425.02}, {265.023,716.388}}; 246 | 247 | int num = check_line_curve_inter(seg, c); 248 | CHECK(num == 0); 249 | 250 | } 251 | 252 | TEST_CASE("line curve intersection adjacent") { 253 | 254 | std::cout.precision(25); 255 | std::cout << std::fixed; 256 | 257 | Segment seg1{{487.548, 643.546}, 258 | {487.548, 352.179}}; 259 | Segment seg2{{196.181, 643.546}, 260 | {487.548, 643.546}}; 261 | Segment seg3{{487.548, 352.179}, 262 | {196.1810, 352.179}}; 263 | CubicBezier c1{{633.231, 497.863}, 264 | {633.231, 578.268}, 265 | {567.953, 643.546}, 266 | {487.548, 643.546}}; 267 | CubicBezier c2{{487.548, 643.546}, 268 | {407.143, 643.546}, 269 | {341.864, 578.268}, 270 | {341.864, 497.863}}; 271 | CubicBezier c3{{341.863999999999976, 497.863000000000000}, 272 | {341.863999999999976, 417.458000000000027}, 273 | {407.142999999999972, 352.179}, 274 | {487.548, 352.179}}; 275 | int num; 276 | num = check_line_curve_inter(seg1, c1); 277 | CHECK(num == 0); 278 | num = check_line_curve_inter(seg1, c2); 279 | CHECK(num == 0); 280 | num = check_line_curve_inter(seg2, c1); 281 | CHECK(num == 0); 282 | num = check_line_curve_inter(seg3, c2); 283 | CHECK(num == 0); 284 | num = check_line_curve_inter(seg3, c1); 285 | CHECK(num == 0); 286 | num = check_line_curve_inter(seg3, c3); 287 | CHECK(num == 0); 288 | num = check_line_curve_inter(seg2, c3); 289 | CHECK(num == 0); 290 | } 291 | 292 | TEST_CASE("line curve intersection almost on line"){ 293 | 294 | CubicBezier c{{2.06669, 0}, {7.59173, 0.158415}, {7.20307, 3.47477}, {-1.03651, 3.2766}}; 295 | Segment seg {{-4.73344, 0.0258641}, {3.36403, 0}}; 296 | 297 | int num = check_line_curve_inter(seg, c); 298 | CHECK( num == 1 ); 299 | } 300 | 301 | TEST_CASE("line curve intersection fuzzy"){ 302 | geometrygen::PointGenerator gen{0, 10, 0, 10}; 303 | 304 | auto gen_segment= [&gen](){ 305 | return Segment{gen(), gen()}; 306 | }; 307 | auto gen_bezier= [&gen](){ 308 | return CubicBezier{gen(), gen(), gen(), gen()}; 309 | }; 310 | int num = 0; 311 | for (int i = 0; i < 10'000; ++i) { 312 | #ifdef DEBUG 313 | std::cout << "processing "<< i << "\n"; 314 | #endif 315 | num += check_line_curve_inter(gen_segment(), gen_bezier()); 316 | } 317 | } 318 | } 319 | 320 | TEST_SUITE("curve curve intersection") { 321 | TEST_CASE("curve curve intersection already axis aligned") { 322 | 323 | CubicBezier b{{-2, 5}, 324 | {1, -2}, 325 | {3, -1}, 326 | {5, 4}}; 327 | CubicBezier q{{0, 0}, 328 | {0.2, 1.2}, 329 | {0.5, 1.9}, 330 | {1.0, 0}}; 331 | 332 | int num = check_curve_curve_inter(b, q); 333 | CHECK(num == 2); 334 | } 335 | 336 | TEST_CASE("test curve curve inter basic") { 337 | CubicBezier B1{{1, 2}, 338 | {3, -4}, 339 | {4, 10}, 340 | {5, 8}};//0 341 | CubicBezier B2{{1, 2}, 342 | {3, -14}, 343 | {4, 20}, 344 | {10, -10}}; //1 345 | CubicBezier Q{{-2, 4}, 346 | {1, -8}, 347 | {8, -5}, 348 | {10, 9}}; 349 | int num = check_curve_curve_inter(B1, Q); 350 | CHECK(num == 0); 351 | num = check_curve_curve_inter(B2, Q); 352 | CHECK(num == 1); 353 | } 354 | 355 | TEST_CASE("test curve curve inter circle approximations") { 356 | 357 | CubicBezier B{{562.098, 517.77}, 358 | {546.036, 628.361}, 359 | {443.209, 705.107}, 360 | {332.618, 689.045}}; 361 | CubicBezier Q{{636.808, 555.905}, 362 | {525.489, 565.729}, 363 | {427.136, 483.328}, 364 | {417.312, 372.009}}; 365 | int num = check_curve_curve_inter(B, Q); 366 | CHECK(num == 1); 367 | } 368 | 369 | TEST_CASE("bezier intersection start-end") { 370 | CubicBezier b1{{1196.340, 617.296}, 371 | {2440.450, 617.296}, 372 | {1196.340, 1748.460}, 373 | {2361.720, 1452.770}}; 374 | CubicBezier b2{{1196.340, 617.296}, 375 | {3321.060, 617.296}, 376 | {236.996, 1452.770}, 377 | {2361.720, 1452.770}}; 378 | int num = check_curve_curve_inter(b1, b2); 379 | CHECK(num == 2); 380 | } 381 | 382 | TEST_CASE("bezier intersection start-end self intersections") { 383 | Point2d p1 = {528.31, 12.1}; 384 | Point2d p2 = {801, 49.5}; 385 | 386 | CubicBezier b1{p1, 387 | {2440.450, 617.296}, 388 | {1196.340, 1748.460}, 389 | p2}; 390 | CubicBezier b2{p1, 391 | {3321.060, 617.296}, 392 | {236.996, 1452.770}, 393 | p2}; 394 | 395 | int num = check_curve_curve_inter(b1, b2); 396 | CHECK(num == 4); 397 | } 398 | 399 | TEST_CASE("bezier intersection start-end 4") { 400 | 401 | Point2d p1 = {528.31, 12.1}; 402 | Point2d p2 = {801, 49.5}; 403 | 404 | CubicBezier b1{p1, 405 | {1196.340, 1748.460}, 406 | {2440.450, 617.296}, 407 | p2}; 408 | CubicBezier b2{p1, 409 | {236.996, 1452.770}, 410 | {3321.060, 617.296}, 411 | p2}; 412 | 413 | int num = check_curve_curve_inter(b1, b2); 414 | CHECK(num == 2); 415 | } 416 | 417 | TEST_CASE("bezier bezier intersections circle_connected") { 418 | 419 | CubicBezier c1{{633.231, 497.863}, 420 | {633.231, 578.268}, 421 | {567.953, 643.546}, 422 | {487.548, 643.546}}; 423 | CubicBezier c2{{487.548, 643.546}, 424 | {407.143, 643.546}, 425 | {341.864, 578.268}, 426 | {341.864, 497.863}}; 427 | 428 | int num = check_curve_curve_inter(c1, c2); 429 | CHECK(num == 0); 430 | } 431 | 432 | TEST_CASE("bezier bezier intersections quarter circle") { 433 | CubicBezier c1{{50, 100},{77.5, 100}, {100, 77.5}, {100, 50}}; 434 | CubicBezier c2{{50, 75},{50, 102.5}, {72.3, 125}, {100, 125}}; 435 | int num = check_curve_curve_inter(c1, c2); 436 | CHECK(num == 1); 437 | } 438 | 439 | TEST_CASE("bezier intersection start-start horizontal"){ 440 | CubicBezier b1{{500, 500}, {525, 550}, {530, 480}, {550, 500}}; 441 | CubicBezier b2{{500, 500}, {530, 490}, {535, 525}, {540, 550}}; 442 | int num = check_curve_curve_inter(b1, b2); 443 | CHECK(num ==1); 444 | } 445 | 446 | TEST_CASE("curve-curve 5 intersections"){ 447 | CubicBezier b1{{308.1, 725.4}, {129.4, 93.6}, {792.2, 941.1}, {594.6, 433.6}}; 448 | CubicBezier b2{{304.2, 734.6}, {168.1, 70.2}, {687, 968.8}, {603.8, 409.5}}; 449 | int num = check_curve_curve_inter(b1, b2); 450 | CHECK(num == 5); 451 | } 452 | 453 | TEST_CASE("curve-curve 9 intersections"){ 454 | CubicBezier b1{{385.1, 514.3}, {802.1, 514.3}, {99.8, 412.8}, {547.4, 412.8}}; 455 | CubicBezier b2{{394.9, 528.3}, {394.9, 169}, {522.7, 739.3}, {522.7, 398.2}}; 456 | int num = check_curve_curve_inter(b1, b2); 457 | CHECK(num == 9); 458 | } 459 | 460 | TEST_CASE("curve-curve 3 intersections"){ 461 | CubicBezier b1{{385.1, 514.3}, {802.1, 514.3}, {99.8, 412.8}, {547.4, 412.8}}; 462 | CubicBezier b2{{385.1, 512.3}, {802.1, 512.3}, {99.8, 414.8}, {547.4, 414.8}}; 463 | 464 | int num = check_curve_curve_inter(b1, b2); 465 | CHECK(num == 3); 466 | } 467 | 468 | TEST_CASE("curve-curve 1 intersection"){ 469 | CubicBezier b1{{464.7, 372.8}, {452.9, 438.5}, {171.1, 533.4}, {121.9, 488.2}}; 470 | CubicBezier b2{{500, 580.6}, {467.8, 580.6},{356.7, 469.5}, {356.7, 437.3} }; 471 | int num = check_curve_curve_inter(b1, b2); 472 | CHECK(num == 1); 473 | } 474 | } 475 | 476 | TEST_SUITE("segment ordering") { 477 | TEST_CASE("bezier ordering visual check") { 478 | 479 | std::string dir = TESTCASE_OTHERS_DIR + "/bezier_ordering/"; 480 | BasicPathLoader pl(dir + "in.svg"); 481 | 482 | auto beziercurve_below = [](const CubicBezier &a, const CubicBezier &b) -> bool { 483 | 484 | detail::SweepPoint a1{a.p0}; 485 | detail::SweepPoint a2{a.p3}; 486 | detail::SweepPoint b1{b.p0}; 487 | detail::SweepPoint b2{b.p3}; 488 | a1.controlp = a.p1; 489 | a2.controlp = a.p2; 490 | b1.controlp = b.p1; 491 | b2.controlp = b.p2; 492 | a1.other_point = &a2; 493 | a2.other_point = &a1; 494 | b1.other_point = &b2; 495 | b2.other_point = &b1; 496 | 497 | a1.set_if_left(); 498 | b1.set_if_left(); 499 | 500 | return curve_below(&a1, &b1); 501 | }; 502 | 503 | auto output_order = [&](bool reflected ) { 504 | BasicPathWriter out(pl.get_dims()); 505 | std::vector v{}; 506 | for (const auto &path: pl.paths) { 507 | v.push_back(bezier_from_str(path)); 508 | double offset = +v.back().p0.x(); 509 | if(reflected) { 510 | auto mirrorf = [&](const Point2d &p) -> Point2d { return {-p.x() + 5 * offset, p.y()}; }; 511 | v.back() = transform(v.back(), mirrorf); 512 | } 513 | out.push_path_str(bezier_to_path_str(v.back()), "none"); 514 | } 515 | 516 | std::sort(v.begin(), v.end(), beziercurve_below); 517 | 518 | int idx = 0; 519 | for (const auto &curve: v) { 520 | out.push_text(beziermap(curve, 0.55), std::to_string(idx)); 521 | idx++; 522 | } 523 | out.write_to(dir + (reflected ? "out_reflected.svg" : "out.svg") ); 524 | }; 525 | output_order(false); 526 | output_order(true); 527 | } 528 | 529 | TEST_CASE("line-bezier ordering") { 530 | Segment seg{{1166.7, 350}, 531 | {1352.3, 505.9}}; 532 | 533 | CubicBezier c{{1166.7, 350}, 534 | {1166.7, 350}, 535 | {1176.7, 148.3}, 536 | {1232.1, 148.3}}; 537 | 538 | detail::SweepPoint s1; 539 | detail::SweepPoint s2; 540 | detail::SweepPoint c1; 541 | detail::SweepPoint c2; 542 | sweeppoint_init(seg, &s1, &s2); 543 | sweeppoint_init(c, &c1, &c2); 544 | 545 | CHECK(curve_below(&c1, &s1)); 546 | CHECK(detail::queue_comp(&c1, &s1)); 547 | } 548 | TEST_CASE("line-bezier ordering horizontal-vertical") { 549 | 550 | Segment seg{{269.023, 425.02}, {560.39, 425.021}}; 551 | 552 | CubicBezier c{{269.023, 425.02}, {269.023, 401.929}, 553 | {274.408, 380.083},{283.989, 360.674}}; 554 | detail::SweepPoint s1; 555 | detail::SweepPoint s2; 556 | detail::SweepPoint c1; 557 | detail::SweepPoint c2; 558 | sweeppoint_init(seg, &s1, &s2); 559 | sweeppoint_init(c, &c1, &c2); 560 | 561 | CHECK(curve_below(&c1, &s1)); 562 | CHECK(detail::queue_comp(&c1, &s1)); 563 | } 564 | 565 | TEST_CASE("line-bezier ordering 2") { 566 | CubicBezier c{{2.523510524871271, -2.822884461255850}, {2.523511628233836, -2.820518727440074}, 567 | {2.523511628233836, -2.820518727440074}, {2.523512179725838, -2.815780285295673}}; 568 | 569 | Segment seg{{2.523510524871271, -2.822884461255850}, {6.795631221097975, 0.000000000000000}}; 570 | detail::SweepPoint s1; 571 | detail::SweepPoint s2; 572 | detail::SweepPoint c1; 573 | detail::SweepPoint c2; 574 | sweeppoint_init(seg, &s1, &s2); 575 | sweeppoint_init(c, &c1, &c2); 576 | CHECK( curve_below(&s1, &c1)); 577 | } 578 | 579 | TEST_CASE("line-bezier ordering overlapping"){ 580 | 581 | Segment seg{{15.068416403647555, 12.765976081320677}, {15.070287752788607, 12.757544852322230}}; 582 | CubicBezier c{seg.first, {15.069040488591890, 12.763164135717986}, 583 | {15.069664271610140, 12.760353726150072}, seg.second}; 584 | 585 | detail::SweepPoint s1; 586 | detail::SweepPoint s2; 587 | detail::SweepPoint c1; 588 | detail::SweepPoint c2; 589 | sweeppoint_init(seg, &s1, &s2); 590 | sweeppoint_init(c, &c1, &c2); 591 | 592 | s1.ptype = s2.ptype = c1.ptype = c2.ptype = contourklip::detail::CLIPPING; 593 | CHECK(detail::curve_below(&s1, &c1) != detail::curve_below(&c1, &s1)); 594 | } 595 | } 596 | 597 | TEST_SUITE("postprocess contour"){ 598 | TEST_CASE("postprocess contour 1") { 599 | 600 | contourklip::Contour c; 601 | c.push_back({0, 0}); 602 | c.push_back({0, 1}); 603 | c.push_back({0.5, 0.5}); 604 | c.push_back({1, 0}); 605 | c.push_back({1, 1}); 606 | c.push_back({0.5, 0.5}); 607 | c.close(); 608 | 609 | std::vector out{}; 610 | auto process = [&](const contourklip::Contour &c) { 611 | out.push_back(c); 612 | }; 613 | postprocess_contour(c, process); 614 | REQUIRE(out.size()==2); 615 | CHECK(out.front().size() == 4); 616 | CHECK(out.back().size() == 4); 617 | } 618 | 619 | TEST_CASE("postprocess contour 2") { 620 | 621 | contourklip::Contour c; 622 | c.push_back({0.5, 0.5}); 623 | c.push_back({0, 0}); 624 | c.push_back({0, 1}); 625 | c.push_back({0.5, 0.5}); 626 | c.push_back({1, 1}); 627 | c.push_back({1, 0}); 628 | c.close(); 629 | 630 | std::vector out{}; 631 | auto process = [&](const contourklip::Contour &c) { 632 | out.push_back(c); 633 | }; 634 | postprocess_contour(c, process); 635 | REQUIRE(out.size()==2); 636 | CHECK(out.front().size() == 4); 637 | CHECK(out.back().size() == 4); 638 | } 639 | 640 | TEST_CASE("postprocess contour 3") { 641 | contourklip::Contour c; 642 | c.push_back({0, 2}); 643 | c.push_back({1, 2}); 644 | c.push_back({2, 2}); 645 | c.push_back({3, 2}); 646 | c.push_back({3, 1}); 647 | c.push_back({4, 1}); 648 | c.push_back({4, 3}); 649 | c.push_back({2, 3}); 650 | c.push_back({2, 2}); 651 | c.push_back({2, 1}); 652 | c.push_back({3, 1}); 653 | c.push_back({3, 0}); 654 | c.push_back({0, 0}); 655 | c.close(); 656 | 657 | std::vector out{}; 658 | auto process = [&](const contourklip::Contour &c) { 659 | out.push_back(c); 660 | }; 661 | postprocess_contour(c, process); 662 | REQUIRE(out.size()==2); 663 | CHECK(out.front().size() == 7); 664 | CHECK(out.back().size() == 7); 665 | } 666 | } 667 | 668 | TEST_SUITE("geometry generation") { 669 | TEST_CASE("random partition") { 670 | double a = 0, b = 2 * M_PI_2; 671 | for (int i = 2; i < 50; ++i) { 672 | for (int seed = 0; seed < 10; ++seed) { 673 | std::vector out = geometrygen::random_partition(a, b, i, 1); 674 | CHECK(out.size() == i - 1); 675 | CHECK(std::is_sorted(out.begin(), out.end())); 676 | } 677 | } 678 | } 679 | 680 | TEST_CASE("point generation"){ 681 | geometrygen::PointGenerator getp{0, 1, 0, 1, 5}; 682 | std::set generated{}; 683 | for (int i = 0; i <25; ++i) { 684 | Point2d p{getp()}; 685 | CHECK(p.x() <= 1); 686 | CHECK(p.x() >= 0); 687 | CHECK(p.y() <= 1); 688 | CHECK(p.y() >= 0); 689 | CHECK(generated.find(p) == generated.end()); 690 | generated.insert(p); 691 | } 692 | } 693 | 694 | TEST_CASE("random contour generation"){ 695 | Contour out; 696 | geometrygen::generate_contour(30, 2., 5., 1.5, 15, out, {}, true); 697 | save_contour("debug_random_contour.svg", out); 698 | } 699 | } 700 | 701 | TEST_CASE("specific testcase") { 702 | std::string case_name = "curves03"; 703 | std::string t_dir = std::string{TESTCASE_DIR} + "/" + case_name + "/"; 704 | 705 | BooleanOpType op = contourklip::INTERSECTION; 706 | std::vector a; 707 | std::vector b; 708 | std::vector a_rev; 709 | std::vector b_rev; 710 | 711 | auto dims = load_testcase(case_name, a, b); 712 | a_rev = a; b_rev = b; 713 | reverse_contours(a_rev); 714 | reverse_contours(b_rev); 715 | 716 | std::vector res; 717 | BasicPathWriter dbg{dims}; 718 | dbg.path_prefx = t_dir + "/"; 719 | 720 | Config config; 721 | PolyClip t(a, b, res, op, config); 722 | #ifdef DEBUG 723 | t.w = &dbg; 724 | t.compute_verbose = true; 725 | #endif 726 | t.compute(); 727 | CHECK(t.success()); 728 | if(!res.empty()) CHECK(contour_area(res.front()) > 0); 729 | BasicPathWriter out{dims}; 730 | if (op == DIVIDE) { 731 | for (const auto &c: res) { 732 | //this way, each contour has its own path 733 | out.push_path_str(multipolygon_to_str(std::vector{c})); 734 | } 735 | } else { 736 | out.push_path_str(multipolygon_to_str(res)); 737 | } 738 | 739 | #ifdef DEBUG 740 | std::cout << "total area of output " << multipolygon_area(res) << "\n"; 741 | std::cout << "consisting of areas:\n"; 742 | for (const auto &c: res) { 743 | std::cout << contour_area(c) << "\n"; 744 | } 745 | save_output(case_name, out, op); 746 | #endif 747 | } -------------------------------------------------------------------------------- /include/geometry_base.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONTOURKLIP_GEOMETRY_BASE_HPP 2 | #define CONTOURKLIP_GEOMETRY_BASE_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "direct_solvers.hpp" 11 | 12 | namespace contourklip { 13 | struct Point2d { 14 | private: 15 | double _x; 16 | double _y; 17 | public: 18 | constexpr Point2d(double x, double y) : _x(x), _y(y) {} 19 | 20 | constexpr Point2d() : _x(0), _y(0) {} 21 | 22 | inline constexpr double x() const { 23 | return _x; 24 | } 25 | 26 | inline constexpr double y() const { 27 | return _y; 28 | } 29 | 30 | inline constexpr double& x() { 31 | return _x; 32 | } 33 | 34 | inline constexpr double& y() { 35 | return _y; 36 | } 37 | 38 | template 39 | friend constexpr double get(const Point2d &p) { 40 | static_assert(idx == 0 || idx == 1); 41 | if constexpr(idx == 0) { 42 | return p.x(); 43 | } else { 44 | return p.y(); 45 | } 46 | } 47 | }; 48 | 49 | template 50 | constexpr double get(const Point2d &p); 51 | 52 | std::ostream &operator<<(std::ostream &o, const Point2d &p) { 53 | return o << "(" << p.x() << ", " << p.y() << ")"; 54 | } 55 | 56 | inline bool operator==(const Point2d &p1, const Point2d &p2) { 57 | return (p1.x() == p2.x()) && (p1.y() == p2.y()); 58 | } 59 | 60 | inline bool operator!=(const Point2d &p1, const Point2d &p2) { return !(p1 == p2); } 61 | 62 | constexpr auto increasing = [](const Point2d &a, const Point2d &b) { 63 | if (a.x() == b.x()) { 64 | return a.y() < b.y(); 65 | } 66 | return a.x() < b.x(); 67 | }; 68 | 69 | bool operator<(const Point2d &a, const Point2d &b) { return increasing(a, b); } 70 | 71 | namespace detail { 72 | 73 | inline bool approx_equal(const Point2d &p1, const Point2d &p2, double eps) { 74 | return std::abs(p1.x() - p2.x()) < eps && std::abs(p1.y() - p2.y()) < eps; 75 | } 76 | 77 | inline double signed_area(const Point2d &p0, const Point2d &p1, const Point2d &p2) { 78 | using namespace directsolvers; 79 | return diff_of_products(p0.x() - p2.x(), p1.y() - p2.y(), p1.x() - p2.x(), p0.y() - p2.y()); 80 | } 81 | // 82 | // auto left_of_line = [](const Point2d &p0, const Point2d &p1, const Point2d &a) -> bool { 83 | // return signed_area(p0, p1, a) > 0; 84 | // }; 85 | // 86 | // auto is_collinear = [](const Point2d &a, const Point2d &b, const Point2d &p) -> bool { 87 | // if (a.x() == b.x()) { 88 | // return a.x() == p.x(); 89 | // } 90 | // if (a.y() == b.y()) { 91 | // return a.y() == p.y(); 92 | // } 93 | // return signed_area(a, b, p) == 0; 94 | // }; 95 | 96 | struct LeftOfLine{ 97 | bool operator()(const Point2d &p0, const Point2d &p1, const Point2d &a) const{ 98 | return signed_area(p0, p1, a) > 0; 99 | } 100 | }; 101 | 102 | struct IsCollinear{ 103 | bool operator()(const Point2d &a, const Point2d &b, const Point2d &p) const{ 104 | if (a.x() == b.x()) { 105 | return a.x() == p.x(); 106 | } 107 | if (a.y() == b.y()) { 108 | return a.y() == p.y(); 109 | } 110 | return signed_area(a, b, p) == 0; 111 | } 112 | }; 113 | 114 | template 115 | inline bool above_line(const Point2d &a, const Point2d &b, const Point2d &p, const Orient2dFunc &on_left = {}) { 116 | return increasing(a, b) ? on_left(a, b, p) : on_left(b, a, p); 117 | } 118 | 119 | double triangle_area(double x1, double y1, double x2, double y2, double x3, double y3) { 120 | return 0.5 * (x1 * (y2 - y3) + 121 | x2 * (y3 - y1) + 122 | x3 * (y1 - y2)); 123 | } 124 | 125 | double quadri_area(const Point2d &a, const Point2d &b, const Point2d &c, const Point2d &d) { 126 | return triangle_area(a.x(), a.y(), b.x(), b.y(), c.x(), c.y()) 127 | + triangle_area(c.x(), c.y(), d.x(), d.y(), a.x(), a.y()); 128 | } 129 | 130 | inline double sqdist(const Point2d &a, const Point2d &b) { 131 | double x = a.x() - b.x(); 132 | double y = a.y() - b.y(); 133 | return x * x + y * y; 134 | } 135 | 136 | inline double dist(const Point2d &a, const Point2d &b) { 137 | return sqrt(sqdist(a, b)); 138 | } 139 | 140 | double sqdist_to(const Point2d &a, const Point2d &b, const Point2d &p) { 141 | double dx = b.x() - a.x(); 142 | double dy = b.y() - a.y(); 143 | double num = dy * p.x() - dx * p.y() + b.x() * a.y() - b.y() * a.x(); 144 | double den = dy * dy + dx * dx; 145 | return num * num / den; 146 | } 147 | 148 | inline bool in_range(double x, double a, double b) { 149 | return a < x && x < b; 150 | } 151 | 152 | inline bool in_range_strict(double a, double b, double x) { 153 | return a < x && x < b; 154 | } 155 | 156 | inline bool in_range_closed(double a, double b, double x) { 157 | return a <= x && x <= b; 158 | } 159 | 160 | inline bool in_interval(double a, double b, double x) { 161 | return a < b ? in_range(x, a, b) : in_range(x, b, a); 162 | } 163 | 164 | inline bool in_box(const Point2d &a, const Point2d &b, const Point2d &p) { 165 | return in_interval(a.x(), b.x(), p.x()) && in_interval(a.y(), b.y(), p.y()); 166 | } 167 | 168 | Point2d basic_intersection(const Point2d &p1, const Point2d &p2, const Point2d &p3, const Point2d &p4) { 169 | double den = ((p1.x() - p2.x()) * (p3.y() - p4.y()) - (p1.y() - p2.y()) * (p3.x() - p4.x())); 170 | double px = ((p1.x() * p2.y() - p1.y() * p2.x()) * (p3.x() - p4.x()) 171 | - (p1.x() - p2.x()) * (p3.x() * p4.y() - p3.y() * p4.x())) 172 | / den; 173 | double py = ((p1.x() * p2.y() - p1.y() * p2.x()) * (p3.y() - p4.y()) - 174 | (p1.y() - p2.y()) * (p3.x() * p4.y() - p3.y() * p4.x())) 175 | / den; 176 | return {px, py}; 177 | } 178 | 179 | class BBox { 180 | public: 181 | double min_x; 182 | double min_y; 183 | double max_x; 184 | double max_y; 185 | 186 | inline bool weak_contains(const Point2d &p) const { 187 | return min_x <= p.x() && p.x() <= max_x 188 | && min_y <= p.y() && p.y() <= max_y; 189 | } 190 | 191 | inline bool strict_contains(const Point2d &p) const { 192 | return min_x < p.x() && p.x() < max_x 193 | && min_y < p.y() && p.y() < max_y; 194 | } 195 | 196 | inline bool strict_contains_x(const double &x) const { 197 | return min_x < x && x < max_x; 198 | } 199 | 200 | inline bool strict_contains_y(const double &y) const { 201 | return min_y < y && y < max_y; 202 | } 203 | 204 | inline bool weak_contains_x(const double &x) const { 205 | return min_x <= x && x <= max_x; 206 | } 207 | 208 | inline bool weak_contains_y(const double &y) const { 209 | return min_y <= y && y <= max_y; 210 | } 211 | 212 | inline bool weak_overlap(const BBox &other) const { 213 | bool vertical = weak_contains_x(other.min_x) 214 | || weak_contains_x(other.max_x) 215 | || other.weak_contains_x(this->min_x) 216 | || other.weak_contains_x(this->max_x); 217 | bool horizontal = weak_contains_y(other.min_y) 218 | || weak_contains_y(other.max_y) 219 | || other.weak_contains_y(this->min_y) 220 | || other.weak_contains_y(this->max_y); 221 | return vertical && horizontal; 222 | } 223 | 224 | inline bool strict_overlap(const BBox &other) const { 225 | if (weak_overlap(other)) { 226 | bool vertical = strict_contains_x(other.min_x) 227 | || strict_contains_x(other.max_x) 228 | || other.strict_contains_x(this->min_x) 229 | || other.strict_contains_x(this->max_x); 230 | bool horizontal = strict_contains_y(other.min_y) 231 | || strict_contains_y(other.max_y) 232 | || other.strict_contains_y(this->min_y) 233 | || other.strict_contains_y(this->max_y); 234 | return vertical || horizontal; 235 | } 236 | return false; 237 | } 238 | 239 | 240 | friend std::ostream &operator<<(std::ostream &o, const BBox &bbox) { 241 | return o << "[" << Point2d{bbox.min_x, bbox.min_y} 242 | << ", " << Point2d{bbox.max_x, bbox.max_y} << "]"; 243 | } 244 | }; 245 | 246 | struct Segment { 247 | Point2d first; 248 | Point2d second; 249 | 250 | friend std::ostream &operator<<(std::ostream &o, const Segment &p) { 251 | return o << "[" << p.first << ", " << p.second << "]"; 252 | } 253 | }; 254 | 255 | inline Point2d linear_map(const Point2d &first, const Point2d &second, double t) { 256 | return {first.x() + t * (second.x() - first.x()), first.y() + t * (second.y() - first.y())}; 257 | } 258 | 259 | inline Point2d linear_map(const Segment &seg, double t) { 260 | return linear_map(seg.first, seg.second, t); 261 | } 262 | 263 | inline bool vertical(const Point2d &a, const Point2d &b) { 264 | return a.x() == b.x(); 265 | } 266 | 267 | inline bool horizontal(const Point2d &a, const Point2d &b) { 268 | return a.y() == b.y(); 269 | } 270 | 271 | template 272 | T segment_tval(const T &a_x, const T &a_y, const T &b_x, const T &b_y, const T &p_x, const T &p_y) { 273 | return a_x; 274 | } 275 | 276 | double segment_tval(const Segment &seg, const Point2d &p) { 277 | if (p == seg.first) return 0; 278 | if (p == seg.second) return 1; 279 | if (seg.second.x() == seg.first.x()) { 280 | return (p.y() - seg.first.y()) / (seg.second.y() - seg.first.y()); 281 | } 282 | return (p.x() - seg.first.x()) / (seg.second.x() - seg.first.x()); 283 | } 284 | 285 | BBox make_bbox(const Segment &seg) { 286 | double min_x = std::min(seg.first.x(), seg.second.x()); 287 | double min_y = std::min(seg.first.y(), seg.second.y()); 288 | double max_x = std::max(seg.first.x(), seg.second.x()); 289 | double max_y = std::max(seg.first.y(), seg.second.y()); 290 | return {min_x, min_y, max_x, max_y}; 291 | } 292 | 293 | struct SegInter { 294 | double t1; 295 | double t2; 296 | Point2d p; 297 | }; 298 | 299 | std::ostream &operator<<(std::ostream &o, const SegInter &q) { 300 | return o << "[" << q.p << " " << q.t1 << " " << q.t2 << "]"; 301 | } 302 | 303 | bool operator==(const SegInter &a, const SegInter &b) { 304 | return a.t1 == b.t1 && a.t2 == b.t2 && a.p == b.p; 305 | } 306 | 307 | // returns a SegInter if the following 3 conditions hold: 308 | // a) the segments do not share any endpoint 309 | // b) the segments are not parallel 310 | // c) at least one of the segments is split in 2 new segments by the other segment 311 | // note that points are passed by value 312 | template 313 | std::optional intersect_segments_detail(Point2d a1, 314 | Point2d a2, 315 | Point2d b1, 316 | Point2d b2) { 317 | if (a1 == b1 318 | || a2 == b2 319 | || a1 == b2 320 | || a2 == b1 321 | ) { 322 | return {}; 323 | } 324 | collinearF collinear; 325 | bool b1_on_a = collinear(a1, a2, b1); 326 | bool b2_on_a = collinear(a1, a2, b2); 327 | if (b1_on_a && b2_on_a) { 328 | return {}; 329 | } 330 | 331 | auto make_inter = [&](double t, double u, const Point2d &p) -> SegInter { 332 | double a = t, b = u; 333 | return SegInter{a, b, p}; 334 | }; 335 | 336 | double x1 = a1.x(), y1 = a1.y(); 337 | double x2 = a2.x(), y2 = a2.y(); 338 | double x3 = b1.x(), y3 = b1.y(); 339 | double x4 = b2.x(), y4 = b2.y(); 340 | 341 | using namespace directsolvers; 342 | double den = diff_of_products((x1 - x2), (y3 - y4), (y1 - y2), (x3 - x4)); 343 | double num1 = diff_of_products((x1 - x3), (y3 - y4), (y1 - y3), (x3 - x4)); 344 | double num2 = diff_of_products((x2 - x1), (y1 - y3), (y2 - y1), (x1 - x3)); 345 | 346 | bool a_notinrange = num1 * den < 0 || std::abs(num1) > std::abs(den); 347 | bool b_notinrange = num2 * den < 0 || std::abs(num2) > std::abs(den); 348 | if (b1_on_a) { 349 | if (a_notinrange) { 350 | return {}; 351 | } 352 | return make_inter(num1 / den, 0, b1); 353 | } 354 | if (b2_on_a) { 355 | if (a_notinrange) { 356 | return {}; 357 | } 358 | return make_inter(num1 / den, 1, b2); 359 | } 360 | bool a1_on_b = collinear(b1, b2, a1); 361 | bool a2_on_b = collinear(b1, b2, a2); 362 | if (a1_on_b) { 363 | if (b_notinrange) { 364 | return {}; 365 | } 366 | return make_inter(0, num2 / den, a1); 367 | } 368 | if (a2_on_b) { 369 | if (b_notinrange) { 370 | return {}; 371 | } 372 | return make_inter(1, num2 / den, a2); 373 | } 374 | if (a_notinrange || b_notinrange) { 375 | return {}; 376 | } 377 | return make_inter(num1 / den, num2 / den, linear_map(a1, a2, num1 / den)); 378 | } 379 | 380 | template 381 | std::optional intersect_segments(Point2d a1, Point2d a2, Point2d b1, Point2d b2) { 382 | if (a1 == b1 383 | || a2 == b2 384 | || a1 == b2 385 | || a2 == b1 386 | ) { 387 | return {}; 388 | } 389 | collinearF collinear; 390 | bool b1_on_a = collinear(a1, a2, b1); 391 | bool b2_on_a = collinear(a1, a2, b2); 392 | if (b1_on_a && b2_on_a) { 393 | return {}; 394 | } 395 | bool a_reversed = false, b_reversed = false; 396 | bool segments_swapped = false; 397 | 398 | if ((a_reversed = !increasing(a1, a2))) { 399 | std::swap(a1, a2); 400 | } 401 | if ((b_reversed = !increasing(b1, b2))) { 402 | std::swap(b1, b2); 403 | } 404 | double dx1 = a2.x() - a1.x(), dy1 = a2.y() - a1.y(); 405 | double dx2 = b2.x() - b1.x(), dy2 = b2.y() - b1.y(); 406 | 407 | if (!increasing({dx1, dy1}, {dx2, dy2})) { 408 | std::swap(a1, b1); 409 | std::swap(a2, b2); 410 | segments_swapped = true; 411 | } 412 | if (auto ret = intersect_segments_detail(a1, a2, b1, b2)) { 413 | if (segments_swapped) { 414 | std::swap(ret->t1, ret->t2); 415 | } 416 | if (a_reversed) { 417 | ret->t1 = 1 - ret->t1; 418 | } 419 | if (b_reversed) { 420 | ret->t2 = 1 - ret->t2; 421 | } 422 | return ret; 423 | } 424 | return {}; 425 | } 426 | 427 | template 428 | std::optional intersect_segments(const Segment &a, const Segment &b) { 429 | return intersect_segments(a.first, a.second, b.first, b.second); 430 | } 431 | 432 | struct CubicBezier { 433 | Point2d p0; 434 | Point2d p1; 435 | Point2d p2; 436 | Point2d p3; 437 | 438 | CubicBezier() = default; 439 | 440 | CubicBezier(const Point2d &p0, const Point2d &p1, const Point2d &p2, const Point2d &p3) : p0(p0), p1(p1), 441 | p2(p2), p3(p3) {} 442 | 443 | explicit CubicBezier(std::array &in) { 444 | p0 = in[0]; 445 | p1 = in[1]; 446 | p2 = in[2]; 447 | p3 = in[3]; 448 | } 449 | 450 | constexpr std::array as_array() const { 451 | return {p0, p1, p2, p3}; 452 | } 453 | 454 | friend std::ostream &operator<<(std::ostream &o, const CubicBezier &p) { 455 | return o << "[" << p.p0 << " " << p.p1 << " " << p.p2 << " " << p.p3 << "]"; 456 | } 457 | 458 | friend bool operator==(const CubicBezier &a, const CubicBezier &b) { 459 | return a.as_array() == b.as_array(); 460 | } 461 | }; 462 | 463 | inline void make_hull_bbox(const CubicBezier &c, BBox &out) { 464 | double min_x = std::min(c.p0.x(), c.p3.x()); 465 | double min_y = std::min(c.p0.y(), c.p3.y()); 466 | double max_x = std::max(c.p0.x(), c.p3.x()); 467 | double max_y = std::max(c.p0.y(), c.p3.y()); 468 | out.min_x = std::min(min_x, std::min(c.p1.x(), c.p2.x())); 469 | out.min_y = std::min(min_y, std::min(c.p1.y(), c.p2.y())); 470 | out.max_x = std::max(max_x, std::max(c.p1.x(), c.p2.x())); 471 | out.max_y = std::max(max_y, std::max(c.p1.y(), c.p2.y())); 472 | } 473 | } 474 | class Contour; 475 | 476 | enum ComponentType { 477 | LINE = 0, 478 | CUBIC_BEZIER = 1, 479 | }; 480 | 481 | /// \brief a simple struct to represent a segment of a path. 482 | struct ContourComponent { 483 | friend class Contour; 484 | private: 485 | ComponentType component_type_; 486 | Point2d c_1_; 487 | Point2d c_2_; 488 | Point2d point_; 489 | public: 490 | /// \brief constructs an instance this representing a line segment of a Contour. 491 | /// If given a Contour c, adding this to it will represent the segment [c.back_point(), this->point()] 492 | /// \param pLast the point_ Point2d representing the end point 493 | explicit ContourComponent(const Point2d &p) : component_type_(LINE), point_(p) {} 494 | 495 | /// \brief constructs an instance representing a cubic bezier segment of a contour. 496 | /// given some first point p, it will represent the bezier [p, c1, c2, point]. 497 | /// \param p_1 the first Point2d control point 498 | /// \param p_2 the second Point2d control point 499 | /// \param p_last the Point2d endpoint 500 | ContourComponent(const Point2d &c_1, 501 | const Point2d &c_2, 502 | const Point2d &p) : 503 | component_type_(CUBIC_BEZIER), c_1_(c_1), c_2_(c_2), point_(p) {} 504 | 505 | Point2d c1() const { 506 | #ifdef DEBUG 507 | assert(segment_shape() == CUBIC_BEZIER); 508 | #endif 509 | return c_1_; 510 | } 511 | 512 | Point2d& c1() { 513 | return c_1_; 514 | } 515 | 516 | Point2d c2() const { 517 | #ifdef DEBUG 518 | assert(segment_shape() == CUBIC_BEZIER); 519 | #endif 520 | return c_2_; 521 | } 522 | 523 | Point2d& c2() { 524 | return c_2_; 525 | } 526 | 527 | Point2d point() const { 528 | return point_; 529 | } 530 | 531 | Point2d &point() { 532 | return point_; 533 | } 534 | 535 | bool bcurve() const { 536 | return segment_shape() == CUBIC_BEZIER; 537 | } 538 | 539 | /// \brief returns the shape type tag associated with this instance. 540 | /// \return the shape type ComponentType 541 | ComponentType segment_shape() const { 542 | return component_type_; 543 | } 544 | 545 | friend bool operator==(const ContourComponent &a, const ContourComponent &b) { 546 | if (a.component_type_ != b.component_type_) { 547 | return false; 548 | } 549 | if (a.bcurve()) { 550 | return a.c1() == b.c1() 551 | && a.c2() == b.c2() 552 | && a.point() == b.point(); 553 | } 554 | return a.point() == b.point(); 555 | } 556 | 557 | private: 558 | /// \brief reverses the control points associated with this, irrespective of the shape type. 559 | void reverse_controlp() { 560 | Point2d temp = c_1_; 561 | c_1_ = c_2_; 562 | c_2_ = temp; 563 | } 564 | 565 | }; 566 | 567 | std::ostream &operator<<(std::ostream &o, const ContourComponent &comp) { 568 | switch (comp.segment_shape()) { 569 | case CUBIC_BEZIER: 570 | return o << comp.c1() << " " << comp.c2() << " " << comp.point(); 571 | case LINE: 572 | return o << comp.point(); 573 | } 574 | return o; 575 | } 576 | 577 | class Contour { 578 | private: 579 | using ContainerType = std::vector; 580 | ContainerType container{}; 581 | public: 582 | Contour() = default; 583 | 584 | explicit Contour(const Point2d &start) { 585 | push_back(start); 586 | } 587 | 588 | Contour(const Point2d &p0, const Point2d &p1, const Point2d &p2, const Point2d &p3) { 589 | push_back(p0); 590 | push_back(p1, p2, p3); 591 | } 592 | 593 | Contour(const Point2d &p0, const Point2d &p1) { 594 | push_back(p0); 595 | push_back(p1); 596 | } 597 | 598 | void push_back(const Point2d &p) { 599 | push_back(ContourComponent(p)); 600 | } 601 | 602 | void push_back(const Point2d &p2, 603 | const Point2d &p3, 604 | const Point2d &p) { 605 | push_back( 606 | ContourComponent{p2, p3, p} 607 | ); 608 | } 609 | 610 | void push_back(const ContourComponent &start) { 611 | container.push_back(start); 612 | } 613 | 614 | ContourComponent operator[](const std::size_t idx) const { 615 | return container[idx]; 616 | } 617 | 618 | ContourComponent& operator[](const std::size_t idx) { 619 | return container[idx]; 620 | } 621 | 622 | std::size_t size() const { 623 | return container.size(); 624 | } 625 | 626 | Point2d front_point() const { 627 | return container.front().point(); 628 | } 629 | 630 | Point2d back_point() const { 631 | return container.back().point(); 632 | } 633 | 634 | 635 | ContourComponent &front() { 636 | return container.front(); 637 | } 638 | 639 | ContourComponent &back() { 640 | return container.back(); 641 | } 642 | 643 | ContourComponent front() const { 644 | return container.front(); 645 | } 646 | 647 | ContourComponent back() const { 648 | return container.back(); 649 | } 650 | 651 | bool is_closed() const { 652 | return front_point() == back_point(); 653 | } 654 | 655 | void close() { 656 | if (!is_closed()) { 657 | this->push_back(front_point()); 658 | } 659 | } 660 | 661 | void reverse() { 662 | std::reverse(container.begin(), container.end()); 663 | for (std::size_t i = 0; i < container.size() - 1; ++i) { 664 | container[i].point() = container[i + 1].point(); 665 | if (container[i].segment_shape() == CUBIC_BEZIER) { 666 | container[i].reverse_controlp(); 667 | } 668 | } 669 | //rotate to the right so that we start with a simple point. 670 | std::rotate(container.rbegin(), container.rbegin() + 1, container.rend()); 671 | } 672 | 673 | template 674 | void forward_segments(Consumer &out) const { 675 | if (container.empty()) { return; } 676 | auto it = container.begin(); 677 | for (auto prev = it++; it != container.end(); prev++, it++) { 678 | auto pair = std::make_pair(*prev, *it); 679 | if (pair.second.segment_shape() == T) { 680 | if constexpr (T == LINE) { 681 | out(pair.first.point(), pair.second.point()); 682 | } else { 683 | out(pair.first.point(), 684 | pair.second.c1(), 685 | pair.second.c2(), 686 | pair.second.point()); 687 | } 688 | } 689 | } 690 | } 691 | 692 | auto begin() const { 693 | return container.begin(); 694 | } 695 | 696 | auto begin() { 697 | return container.begin(); 698 | } 699 | 700 | auto end() const { 701 | return container.end(); 702 | } 703 | 704 | auto end() { 705 | return container.end(); 706 | } 707 | 708 | friend bool operator==(const Contour &a, const Contour &b) { 709 | return a.container == b.container; 710 | } 711 | 712 | friend std::ostream &operator<<(std::ostream &o, const Contour &c) { 713 | for (const auto &seg: c) { 714 | std::cout << seg << '\n'; 715 | } 716 | return o; 717 | } 718 | }; 719 | 720 | 721 | namespace detail { 722 | std::tuple contourbbox(const Contour &a) { 723 | double min_x = a.front_point().x(); 724 | double max_x = min_x; 725 | double min_y = a.front_point().y(); 726 | double max_y = min_y; 727 | for (const auto &seg: a) { 728 | min_x = std::min(min_x, seg.point().x()); 729 | min_y = std::min(min_y, seg.point().y()); 730 | max_x = std::max(max_x, seg.point().x()); 731 | max_y = std::max(max_y, seg.point().y()); 732 | switch (seg.segment_shape()) { 733 | case LINE: 734 | continue; 735 | case CUBIC_BEZIER: 736 | min_x = std::min(min_x, std::min(seg.c1().x(), seg.c2().x())); 737 | min_y = std::min(min_y, std::min(seg.c1().y(), seg.c2().y())); 738 | max_x = std::max(max_x, std::max(seg.c1().x(), seg.c2().x())); 739 | max_y = std::max(max_y, std::max(seg.c1().y(), seg.c2().y())); 740 | continue; 741 | } 742 | } 743 | return {min_x, min_y, max_x, max_y}; 744 | } 745 | 746 | double bezier_area(const Point2d &p0, const Point2d &p1, 747 | const Point2d &p2, const Point2d p3) { 748 | double x0 = p0.x(), y0 = p0.y(), x1 = p1.x(), y1 = p1.y(), 749 | x2 = p2.x(), y2 = p2.y(), x3 = p3.x(), y3 = p3.y(); 750 | return (x0 * (-2 * y1 - y2 + 3 * y3) 751 | + x1 * (2 * y0 - y2 - y3) 752 | + x2 * (y0 + y1 - 2 * y3) 753 | + x3 * (-3 * y0 + y1 + 2 * y2) 754 | ) * 3. / 20.; 755 | } 756 | 757 | double contour_area(const Contour &c) { 758 | if (c.size() < 2) return 0.; 759 | double area = 0.0; 760 | for (std::size_t i = 0; i < c.size() - 1; ++i) { 761 | area += c[i].point().x() * c[i + 1].point().y() 762 | - c[i + 1].point().x() * c[i].point().y(); 763 | if (c[i + 1].segment_shape() == CUBIC_BEZIER) { 764 | double t = bezier_area(c[i].point(), c[i + 1].c1(), 765 | c[i + 1].c2(), c[i + 1].point()); 766 | //mult. by 2 since at the end we div by 2. 767 | area -= 2 * t; 768 | } 769 | } 770 | if (!c.is_closed()) { 771 | // we only have an implicit line segment 772 | area += c.back_point().x() * c.front_point().y() 773 | - c.back_point().x() * c.front_point().y(); 774 | } 775 | return 0.5 * area; 776 | } 777 | 778 | double multipolygon_area(const std::vector &poly) { 779 | double area = 0; 780 | for (const auto &c: poly) { 781 | area += contour_area(c); 782 | } 783 | return area; 784 | } 785 | } 786 | } 787 | #endif //CONTOURKLIP_GEOMETRY_BASE_HPP --------------------------------------------------------------------------------