Added scale_{from,to}_clipper().
This commit is contained in:
parent
3c095a065b
commit
35e33bb37c
2 changed files with 100 additions and 0 deletions
|
|
@ -26,6 +26,7 @@ import copy as _copy
|
|||
import unicodedata as _unicodedata
|
||||
import time as _time
|
||||
import warnings as _warnings
|
||||
import numbers as _numbers
|
||||
|
||||
from cython.operator cimport dereference as deref
|
||||
|
||||
|
|
@ -512,6 +513,54 @@ def ReversePaths(paths):
|
|||
return _from_clipper_paths(c_paths)
|
||||
|
||||
|
||||
def scale_to_clipper(path_or_paths, scale = 2 ** 31):
|
||||
"""
|
||||
Take a path or list of paths with coordinates represented by floats and scale them using the specified factor. This function can be user to convert paths to a representation which is more appropriate for Clipper.
|
||||
|
||||
Clipper, and thus Pyclipper, uses 64-bit integers to represent coordinates internally. The actual supported range (+/- 2 ** 62) is a bit smaller than the maximal values for this type. To operate on paths which use fractional coordinates, it is necessary to translate them from and to a representation which does not depend on floats. This can be done using this function and it's reverse, `scale_from_clipper()`.
|
||||
|
||||
For details, see http://www.angusj.com/delphi/clipper/documentation/Docs/Overview/Rounding.htm.
|
||||
|
||||
For example, to perform a clip operation on two polygons, the arguments to `Pyclipper.AddPath()` need to be wrapped in `scale_to_clipper()` while the return value needs to be converted back with `scale_from_clipper()`:
|
||||
|
||||
>>> pc = Pyclipper()
|
||||
>>> path = [[0, 0], [1, 0], [1 / 2, (3 / 4) ** (1 / 2)]] # A triangle.
|
||||
>>> clip = [[0, 1 / 3], [1, 1 / 3], [1, 2 / 3], [0, 1 / 3]] # A rectangle.
|
||||
>>> pc.AddPath(scale_to_clipper(path), PT_SUBJECT)
|
||||
>>> pc.AddPath(scale_to_clipper(clip), PT_CLIP)
|
||||
>>> scale_from_clipper(pc.Execute(CT_INTERSECTION))
|
||||
[[[0.6772190444171429, 0.5590730146504939], [0.2383135547861457, 0.41277118446305394], [0.19245008938014507, 0.3333333330228925], [0.8075499106198549, 0.3333333330228925]]]
|
||||
|
||||
:param path_or_paths: Either a list of paths or a path. A path is a list of tuples of numbers.
|
||||
:param scale: The factor with which to multiply coordinates before converting rounding them to ints. The default will give you a range of +/- 2 ** 31 with a precision of 2 ** -31.
|
||||
"""
|
||||
|
||||
def scale_value(x):
|
||||
if isinstance(x, _numbers.Real):
|
||||
return <cInt>(<double>x * scale)
|
||||
else:
|
||||
return [scale_value(i) for i in x]
|
||||
|
||||
return scale_value(path_or_paths)
|
||||
|
||||
|
||||
def scale_from_clipper(path_or_paths, scale = 2 ** 31):
|
||||
"""
|
||||
Take a path or list of paths with coordinates represented by ints and scale them back to a fractional representation. This function does the inverse of `scale_to_clipper()`.
|
||||
|
||||
:param path_or_paths: Either a list of paths or a path. A path is a list of tuples of numbers.
|
||||
:param scale: The factor by which to divide coordinates when converting them to floats.
|
||||
"""
|
||||
|
||||
def scale_value(x):
|
||||
if isinstance(x, _numbers.Real):
|
||||
return <double>x / scale
|
||||
else:
|
||||
return [scale_value(i) for i in x]
|
||||
|
||||
return scale_value(path_or_paths)
|
||||
|
||||
|
||||
cdef class Pyclipper:
|
||||
|
||||
"""Wraps the Clipper class.
|
||||
|
|
|
|||
|
|
@ -340,6 +340,57 @@ class TestScalingFactorWarning(TestCase):
|
|||
self.pc.AddPaths([PATH_SUBJ_1, PATH_SUBJ_2], poly_type=pyclipper.PT_SUBJECT)
|
||||
|
||||
|
||||
class TestScalingFunctions(TestCase):
|
||||
scale = 2 ** 31
|
||||
path = [(0, 0), (1, 1)]
|
||||
paths = [path] * 3
|
||||
|
||||
def test_value_scale_to(self):
|
||||
value = 0.5
|
||||
res = pyclipper.scale_to_clipper(value, self.scale)
|
||||
|
||||
assert isinstance(res, int)
|
||||
assert res == int(value * self.scale)
|
||||
|
||||
def test_value_scale_from(self):
|
||||
value = 1000000000000
|
||||
res = pyclipper.scale_from_clipper(value, self.scale)
|
||||
|
||||
assert isinstance(res, float)
|
||||
# Convert to float to get "normal" division in Python < 3.
|
||||
assert res == float(value) / self.scale
|
||||
|
||||
def test_path_scale_to(self):
|
||||
res = pyclipper.scale_to_clipper(self.path)
|
||||
|
||||
assert len(res) == len(self.path)
|
||||
assert all(isinstance(i, list) for i in res)
|
||||
assert all(isinstance(j, int) for i in res for j in i)
|
||||
|
||||
def test_path_scale_from(self):
|
||||
res = pyclipper.scale_from_clipper(self.path)
|
||||
|
||||
assert len(res) == len(self.path)
|
||||
assert all(isinstance(i, list) for i in res)
|
||||
assert all(isinstance(j, float) for i in res for j in i)
|
||||
|
||||
def test_paths_scale_to(self):
|
||||
res = pyclipper.scale_to_clipper(self.paths)
|
||||
|
||||
assert len(res) == len(self.paths)
|
||||
assert all(isinstance(i, list) for i in res)
|
||||
assert all(isinstance(j, list) for i in res for j in i)
|
||||
assert all(isinstance(k, int) for i in res for j in i for k in j)
|
||||
|
||||
def test_paths_scale_from(self):
|
||||
res = pyclipper.scale_from_clipper(self.paths)
|
||||
|
||||
assert len(res) == len(self.paths)
|
||||
assert all(isinstance(i, list) for i in res)
|
||||
assert all(isinstance(j, list) for i in res for j in i)
|
||||
assert all(isinstance(k, float) for i in res for j in i for k in j)
|
||||
|
||||
|
||||
def _do_solutions_match(paths_1, paths_2, factor=None):
|
||||
if len(paths_1) != len(paths_2):
|
||||
return False
|
||||
|
|
|
|||
Loading…
Reference in a new issue