├── .gitmodules ├── LICENSE ├── README.md ├── cavaliercontours ├── __init__.py └── cavaliercontours.py ├── examples ├── offset.py ├── polyline.npy └── simple.py ├── generate_package.sh ├── generate_shared_lib.sh ├── images └── offset.jpg └── setup.py /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libcpp/CavalierContours"] 2 | path = libcpp/CavalierContours 3 | url = git@github.com:jbuckmccready/CavalierContours.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jedidiah Buck McCready 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cavaliercontours-python 2 | 3 | A python binding for the [CavalierContours C++ library](https://github.com/jbuckmccready/CavalierContours). 4 | 5 |

6 | 7 |

8 | 9 | ## Installation 10 | 11 | `pip install cavaliercontours-python` 12 | 13 | ## Minimal example 14 | 15 | ```python 16 | #!/usr/bin/env python3 17 | import cavaliercontours as cavc 18 | 19 | vertex_data = [[45., 30., 10., 10., 0., 0., 45.], # x 20 | [20., 35., 35., 50., 50., 0., 0.], # y 21 | [0.41421, 0., 0., 0., 0., 0., 0.]] # bulge 22 | 23 | polyline = cavc.Polyline(vertex_data, is_closed=True) 24 | 25 | print(polyline.is_closed()) 26 | print(polyline.vertex_count()) 27 | print(polyline.get_path_length()) 28 | print(polyline.get_area()) 29 | 30 | polyline_list = polyline.parallel_offset(delta=3.0, check_self_intersect=False) 31 | print(polyline_list[0].vertex_data()) 32 | 33 | # ... 34 | ``` 35 | -------------------------------------------------------------------------------- /cavaliercontours/__init__.py: -------------------------------------------------------------------------------- 1 | from .cavaliercontours import Polyline 2 | -------------------------------------------------------------------------------- /cavaliercontours/cavaliercontours.py: -------------------------------------------------------------------------------- 1 | import ctypes, pathlib 2 | import numpy as np 3 | 4 | module_root = pathlib.Path(__file__).resolve().parent 5 | libname = module_root / "lib/libCavalierContours.so" 6 | c_lib = ctypes.CDLL(libname) 7 | 8 | class _PointStruct(ctypes.Structure): 9 | _fields_ = [('x', ctypes.c_double), 10 | ('y', ctypes.c_double)] 11 | 12 | class _VertexStruct(ctypes.Structure): 13 | _fields_ = [('x', ctypes.c_double), 14 | ('y', ctypes.c_double), 15 | ('bulge', ctypes.c_double)] 16 | 17 | # CAVC_PLINE_NEW 18 | c_lib.cavc_pline_new.argtypes = [ 19 | np.ctypeslib.ndpointer(dtype=np.double, ndim=2, flags='C_CONTIGUOUS'), 20 | ctypes.c_uint32, 21 | ctypes.c_int] 22 | c_lib.cavc_pline_new.restype = ctypes.c_void_p 23 | 24 | # CAVC_PLINE_DELETE 25 | c_lib.cavc_pline_delete.argtypes = [ctypes.c_void_p] 26 | # void return 27 | 28 | # CAVC_PLINE_CAPACITY 29 | c_lib.cavc_pline_capacity.argtypes = [ctypes.c_void_p] 30 | c_lib.cavc_pline_capacity.restype = ctypes.c_uint32 31 | 32 | # CAVC_PLINE_SET_CAPACITY 33 | c_lib.cavc_pline_set_capacity.argtypes = [ctypes.c_void_p, ctypes.c_uint32] 34 | # void return 35 | 36 | # CAVC_PLINE_VERTEX_COUNT 37 | c_lib.cavc_pline_vertex_count.argtypes = [ctypes.c_void_p] 38 | c_lib.cavc_pline_vertex_count.restype = ctypes.c_uint32 39 | 40 | # CAVC_PLINE_VERTEX_DATA 41 | c_lib.cavc_pline_vertex_data.argtypes = [ 42 | ctypes.c_void_p, 43 | np.ctypeslib.ndpointer(dtype=np.double, ndim=2, flags='C_CONTIGUOUS')] 44 | # void return 45 | 46 | # CAVC_PLINE_IS_CLOSED 47 | c_lib.cavc_pline_is_closed.argtypes = [ctypes.c_void_p] 48 | c_lib.cavc_pline_is_closed.restype = ctypes.c_int 49 | 50 | # CAVC_PLINE_SET_VERTEX_DATA 51 | c_lib.cavc_pline_set_vertex_data.argtypes = [ 52 | ctypes.c_void_p, 53 | np.ctypeslib.ndpointer(dtype=np.double, ndim=2, flags='C_CONTIGUOUS'), 54 | ctypes.c_uint32] 55 | # void return 56 | 57 | # CAVC_PLINE_ADD_VERTEX 58 | c_lib.cavc_pline_add_vertex.argtypes = [ctypes.c_void_p, _VertexStruct] 59 | # void return 60 | 61 | # CAVC_PLINE_REMOVE_RANGE 62 | c_lib.cavc_pline_remove_range.argtypes = [ 63 | ctypes.c_void_p, 64 | ctypes.c_uint32, 65 | ctypes.c_uint32] 66 | # void return 67 | 68 | # CAVC_PLINE_CLEAR 69 | c_lib.cavc_pline_clear.argtypes = [ctypes.c_void_p] 70 | # void return 71 | 72 | # CAVC_PLINE_SET_IS_CLOSED 73 | c_lib.cavc_pline_set_is_closed.argtypes = [ctypes.c_void_p, ctypes.c_int] 74 | # void return 75 | 76 | # CAVC_PLINE_LIST_DELETE 77 | c_lib.cavc_pline_list_delete.argtypes = [ctypes.c_void_p] 78 | # void return 79 | 80 | # CAVC_PLINE_LIST_COUNT 81 | c_lib.cavc_pline_list_count.argtypes = [ctypes.c_void_p] 82 | c_lib.cavc_pline_list_count.restype = ctypes.c_uint32 83 | 84 | # CAVC_PLINE_LIST_GET 85 | c_lib.cavc_pline_list_get.argtypes = [ctypes.c_void_p, ctypes.c_uint32] 86 | c_lib.cavc_pline_list_get.restype = ctypes.c_void_p 87 | 88 | # CAVC_PLINE_LIST_RELEASE 89 | c_lib.cavc_pline_list_release.argtypes = [ctypes.c_void_p, ctypes.c_uint32] 90 | c_lib.cavc_pline_list_release.restype = ctypes.c_void_p 91 | 92 | # CAVC_PARALLEL_OFFSET 93 | c_lib.cavc_parallel_offset.argtypes = [ 94 | ctypes.c_void_p, 95 | ctypes.c_double, 96 | ctypes.c_void_p, 97 | ctypes.c_int] 98 | # void return 99 | 100 | # CAVC_COMBINE_PLINES 101 | c_lib.cavc_combine_plines.argtypes = [ 102 | ctypes.c_void_p, 103 | ctypes.c_void_p, 104 | ctypes.c_int, 105 | ctypes.c_void_p, 106 | ctypes.c_void_p] 107 | # void return 108 | 109 | # CAVC_GET_PATH_LENGTH 110 | c_lib.cavc_get_path_length.argtypes = [ctypes.c_void_p] 111 | c_lib.cavc_get_path_length.restype = ctypes.c_double 112 | 113 | # CAVC_GET_AREA 114 | c_lib.cavc_get_area.argtypes = [ctypes.c_void_p] 115 | c_lib.cavc_get_area.restype = ctypes.c_double 116 | 117 | # CAVC_GET_WINDING_NUMBER 118 | c_lib.cavc_get_winding_number.argtypes = [ctypes.c_void_p, _PointStruct] 119 | c_lib.cavc_get_winding_number.restype = ctypes.c_int 120 | 121 | # CAVC_GET_EXTENTS 122 | c_lib.cavc_get_extents.argtypes = [ 123 | ctypes.c_void_p, 124 | ctypes.c_void_p, 125 | ctypes.c_void_p, 126 | ctypes.c_void_p, 127 | ctypes.c_void_p] 128 | # void return 129 | 130 | # CAVC_GET_CLOSEST_POINT 131 | c_lib.cavc_get_closest_point.argtypes = [ 132 | ctypes.c_void_p, 133 | _PointStruct, 134 | ctypes.c_void_p, 135 | ctypes.c_void_p, 136 | ctypes.c_void_p] 137 | # void return 138 | 139 | class Polyline: 140 | def __init__(self, vertex_data, is_closed): 141 | vertex_data = np.array(vertex_data).transpose().astype(np.double) 142 | self._c_pline_ptr = c_lib.cavc_pline_new( 143 | np.ascontiguousarray(vertex_data), vertex_data.shape[0], is_closed) 144 | 145 | def __del__(self): 146 | c_lib.cavc_pline_delete(self._c_pline_ptr) 147 | 148 | def capacity(self): 149 | return c_lib.cavc_pline_capacity(self._c_pline_ptr) 150 | 151 | def set_capacity(self, size): 152 | c_lib.cavc_pline_set_capacity(self._c_pline_ptr, size) 153 | 154 | def vertex_count(self): 155 | return c_lib.cavc_pline_vertex_count(self._c_pline_ptr) 156 | 157 | def vertex_data(self): 158 | vertex_data = np.empty(shape=(self.vertex_count(), 3)) 159 | c_lib.cavc_pline_vertex_data(self._c_pline_ptr, vertex_data) 160 | return np.ctypeslib.as_array(vertex_data).transpose() 161 | 162 | def is_closed(self): 163 | return bool(c_lib.cavc_pline_is_closed(self._c_pline_ptr)) 164 | 165 | def set_vertex_data(self, vertex_data): 166 | vertex_data = np.array(vertex_data).transpose().astype(np.double) 167 | c_lib.cavc_pline_set_vertex_data(self._c_pline_ptr, vertex_data, 168 | vertex_data.shape[0]) 169 | 170 | def add_vertex(self, vertex): 171 | c_lib.cavc_pline_add_vertex(self._c_pline_ptr, 172 | _VertexStruct(vertex[0], vertex[1], vertex[2])) 173 | 174 | def remove_range(self, start_index, count): 175 | c_lib.cavc_pline_remove_range(self._c_pline_ptr, start_index, count) 176 | 177 | def clear(self): 178 | c_lib.cavc_pline_clear(self._c_pline_ptr) 179 | 180 | def set_is_closed(self, is_closed): 181 | c_lib.cavc_pline_set_is_closed(self._c_pline_ptr, is_closed) 182 | 183 | def parallel_offset(self, delta, check_self_intersect): 184 | """Return a list of Polyline.""" 185 | delta = ctypes.c_double(delta) 186 | ret_ptr = ctypes.pointer(ctypes.c_void_p()) 187 | flags = ctypes.c_int(1 if check_self_intersect else 0) 188 | c_lib.cavc_parallel_offset(self._c_pline_ptr, delta, ret_ptr, flags) 189 | return Polyline._extract_plines(ret_ptr.contents) 190 | 191 | def combine_plines(self, other, combine_mode): 192 | """For union combine_mode = 0 193 | For exclude combine_mode = 1 194 | For intersect combine_mode = 2 195 | For XOR combine_mode = 3 196 | Return two lists of Polyline as a tuple (remaining, subtracted). 197 | """ 198 | combine_mode = ctypes.c_int(combine_mode) 199 | remaining = ctypes.pointer(ctypes.c_void_p()) 200 | subtracted = ctypes.pointer(ctypes.c_void_p()) 201 | c_lib.cavc_combine_plines(self._c_pline_ptr, other._c_pline_ptr, 202 | combine_mode, remaining, subtracted) 203 | return {Polyline._extract_plines(remaining.contents), 204 | Polyline._extract_plines(subtracted.contents)} 205 | 206 | def get_path_length(self): 207 | return c_lib.cavc_get_path_length(self._c_pline_ptr) 208 | 209 | def get_area(self): 210 | return c_lib.cavc_get_area(self._c_pline_ptr) 211 | 212 | def get_winding_number(self, point): 213 | return c_lib.cavc_get_winding_number(self._c_pline_ptr, 214 | _PointStruct(point[0], point[1])) 215 | 216 | def get_extents(self): 217 | """Return (min_x, min_y, max_x, max_y) tuple.""" 218 | min_x = ctypes.pointer(ctypes.c_double()) 219 | min_y = ctypes.pointer(ctypes.c_double()) 220 | max_x = ctypes.pointer(ctypes.c_double()) 221 | max_y = ctypes.pointer(ctypes.c_double()) 222 | c_lib.cavc_get_extents(self._c_pline_ptr, min_x, min_y, max_x, max_y) 223 | return (min_x.contents.value, min_y.contents.value, 224 | max_x.contents.value, max_y.contents.value) 225 | 226 | def get_closest_point(self, point): 227 | """Return (closest_start_index, closest_point, distance) tuple.""" 228 | closest_start_index_ptr = ctypes.pointer(ctypes.c_uint32()) 229 | closest_point_ptr = ctypes.pointer(_PointStruct()) 230 | distance_ptr = ctypes.pointer(ctypes.c_double()) 231 | c_lib.cavc_get_closest_point( 232 | self._c_pline_ptr, 233 | _PointStruct(point[0], point[1]), 234 | closest_start_index_ptr, 235 | closest_point_ptr, 236 | distance_ptr) 237 | closest_point = closest_point_ptr.contents 238 | return (closest_start_index_ptr.contents.value, 239 | (closest_point.x, closest_point.y), 240 | distance_ptr.contents.value) 241 | 242 | def _extract_plines(c_pline_list_ptr): 243 | """Internal helper to convert cavc_pline_list to python list.""" 244 | plines = [] 245 | count = c_lib.cavc_pline_list_count(c_pline_list_ptr) 246 | for i in reversed(range(count)): 247 | c_pline_ptr = c_lib.cavc_pline_list_release(c_pline_list_ptr, i) 248 | bare_cavc_pline = Polyline.__new__(Polyline) 249 | bare_cavc_pline._c_pline_ptr = c_pline_ptr 250 | plines.append(bare_cavc_pline) 251 | c_lib.cavc_pline_list_delete(c_pline_list_ptr) 252 | return plines 253 | -------------------------------------------------------------------------------- /examples/offset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import cavaliercontours as cavc 4 | from PyQt5 import QtGui 5 | import pyqtgraph as pg 6 | import numpy as np 7 | import math 8 | 9 | def polyline2segments(polyline): 10 | precision = 1e-3 11 | points = [] 12 | vertex_data = polyline.vertex_data() 13 | n = vertex_data.shape[1] 14 | for i in range(n - int(not polyline.is_closed())): 15 | a = vertex_data[:2,i] 16 | b = vertex_data[:2,(i+1)%n] 17 | bulge = vertex_data[2,i] 18 | if points: 19 | points.pop(-1) 20 | if math.isclose(bulge, 0): 21 | points += [a, b] 22 | else: 23 | rot = np.array([[0,-1], 24 | [1, 0]]) 25 | on_right = bulge >= 0 26 | if not on_right: 27 | rot = -rot 28 | bulge = abs(bulge) 29 | ab = b-a 30 | chord = np.linalg.norm(ab) 31 | radius = chord * (bulge + 1. / bulge) / 4 32 | center_offset = radius - chord * bulge / 2 33 | center = a + ab/2 + center_offset / chord * rot.dot(ab) 34 | 35 | a_dir = a - center 36 | b_dir = b - center 37 | rad_start = math.atan2(a_dir[1], a_dir[0]) 38 | rad_end = math.atan2(b_dir[1], b_dir[0]) 39 | 40 | if not math.isclose(rad_start, rad_end): 41 | if on_right != (rad_start < rad_end): 42 | if on_right: 43 | rad_start -= 2*math.pi 44 | else: 45 | rad_end -= 2*math.pi 46 | 47 | rad_len = abs(rad_end - rad_start) 48 | if radius > precision: 49 | max_angle = 2 * math.acos(1.0 - precision / radius) 50 | else: 51 | max_angle = math.pi 52 | nb_segments = max(2, math.ceil(rad_len / max_angle) + 1) 53 | 54 | angles = np.linspace(rad_start, rad_end, nb_segments + 1) 55 | arc_data = (center.reshape(2,1) + radius * 56 | np.vstack((np.cos(angles), np.sin(angles)))) 57 | points += np.transpose(arc_data).tolist() 58 | return np.transpose(np.array(points)) 59 | 60 | if __name__ == '__main__': 61 | vertex_data = np.load('polyline.npy') 62 | polyline = cavc.Polyline(vertex_data, is_closed=True) 63 | 64 | # create a list of polylines for different offset values 65 | polylines = [polyline] 66 | for i in range(10, 50, 10): 67 | polylines += polyline.parallel_offset(i, False) 68 | 69 | # transform polylines with bulge into straight lines only 70 | line_arrays = [polyline2segments(p) for p in polylines] 71 | 72 | # aggregate all line arrays for display with 'connect' array 73 | all_lines = np.empty((2,0), dtype=np.float) 74 | connect = np.empty(0, dtype=np.bool) 75 | for arr in line_arrays: 76 | connected = np.ones(arr.shape[1], dtype=np.bool) 77 | connected[-1] = False 78 | connect = np.concatenate((connect, connected)) 79 | all_lines = np.concatenate((all_lines, arr), axis=1) 80 | 81 | # pyqtgraph display 82 | pg.setConfigOption('antialias', True) 83 | app = QtGui.QApplication([]) 84 | plot = pg.PlotWidget() 85 | plot.setAspectLocked() 86 | curve = pg.PlotCurveItem([], [], pen=pg.mkPen(color=(80, 200, 255), width=2)) 87 | plot.addItem(curve) 88 | curve.setData(all_lines[0], all_lines[1], connect=connect) 89 | plot.show() 90 | app.exec_() 91 | -------------------------------------------------------------------------------- /examples/polyline.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proto3/cavaliercontours-python/77bcc5c98ef90987fa72c921659851cc36626414/examples/polyline.npy -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import cavaliercontours as cavc 3 | 4 | vertex_data = [[45., 30., 10., 10., 0., 0., 45.], # x 5 | [20., 35., 35., 50., 50., 0., 0.], # y 6 | [0.41421, 0., 0., 0., 0., 0., 0.]] # bulge 7 | 8 | polyline = cavc.Polyline(vertex_data, is_closed=True) 9 | 10 | print('is_closed\t:', polyline.is_closed()) 11 | print('vertex_count\t:', polyline.vertex_count()) 12 | print('path_length\t:', polyline.get_path_length()) 13 | print('area\t\t:', polyline.get_area()) 14 | 15 | point = (10., 20.) 16 | print('winding_number\t:', polyline.get_winding_number(point)) 17 | print('extents\t\t:', polyline.get_extents()) 18 | point = (50., 40.) 19 | print('closest_point\t:', polyline.get_closest_point(point)) 20 | 21 | polyline_list = polyline.parallel_offset(delta=-3.0, check_self_intersect=False) 22 | print('offset get_area\t:', polyline_list[0].get_area()) 23 | print('offset vertex_data:\n', polyline_list[0].vertex_data()) 24 | 25 | # combine_plines(self, other, combine_mode) 26 | -------------------------------------------------------------------------------- /generate_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python3 setup.py sdist bdist_wheel 3 | -------------------------------------------------------------------------------- /generate_shared_lib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | base_dir=$(dirname $(realpath $0)) 4 | cavc_dir=$base_dir/libcpp/CavalierContours 5 | cavc_build_dir=$base_dir/libcpp/build 6 | dst_dir=$base_dir/cavaliercontours/lib 7 | 8 | mkdir -p $cavc_build_dir 9 | cd $cavc_build_dir 10 | cmake $cavc_dir 11 | make -j$(nproc) CavalierContours 12 | mkdir -p $dst_dir 13 | cp libCavalierContours.so $dst_dir 14 | echo "libCavalierContours.so generated into "$dst_dir 15 | -------------------------------------------------------------------------------- /images/offset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proto3/cavaliercontours-python/77bcc5c98ef90987fa72c921659851cc36626414/images/offset.jpg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="cavaliercontours-python", 8 | version="0.0.1", 9 | author="Lucas Felix", 10 | author_email="lucas.felix0738@gmail.com", 11 | description="Python binding to the CavalierContours C++ library", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/proto3/cavaliercontours-python", 15 | packages=setuptools.find_packages(), 16 | package_data={'cavaliercontours': ['lib/libCavalierContours.so']}, 17 | classifiers=[ 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: POSIX :: Linux", 21 | ], 22 | python_requires='>=3.6', 23 | install_requires=[ 24 | 'numpy', 25 | ], 26 | ) 27 | --------------------------------------------------------------------------------