├── .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 |
--------------------------------------------------------------------------------