From de1eb22b166478c8cdb29ac79c0e5f8bfae2db3d Mon Sep 17 00:00:00 2001 From: Bouhek Date: Thu, 1 Feb 2024 13:04:40 +0100 Subject: [PATCH 1/2] Fix floating point error in bezier bbox calculation --- svgpathtools/bezier.py | 4 +-- svgpathtools/constants.py | 3 +++ test/test_bezier.py | 52 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 svgpathtools/constants.py diff --git a/svgpathtools/bezier.py b/svgpathtools/bezier.py index 7c82834..6b03602 100644 --- a/svgpathtools/bezier.py +++ b/svgpathtools/bezier.py @@ -10,7 +10,7 @@ # Internal dependencies from .polytools import real, imag, polyroots, polyroots01 - +from .constants import FLOAT_EPSILON # Evaluation ################################################################## @@ -171,7 +171,7 @@ def bezier_real_minmax(p): if len(p) == 4: # cubic case a = [p.real for p in p] denom = a[0] - 3*a[1] + 3*a[2] - a[3] - if denom != 0: + if abs(denom) > FLOAT_EPSILON: # check that denom != 0 accounting for floating point error delta = a[1]**2 - (a[0] + a[1])*a[2] + a[2]**2 + (a[0] - a[1])*a[3] if delta >= 0: # otherwise no local extrema sqdelta = sqrt(delta) diff --git a/svgpathtools/constants.py b/svgpathtools/constants.py new file mode 100644 index 0000000..426fc22 --- /dev/null +++ b/svgpathtools/constants.py @@ -0,0 +1,3 @@ +"""This submodule contains constants used throughout the project.""" + +FLOAT_EPSILON = 1e-12 \ No newline at end of file diff --git a/test/test_bezier.py b/test/test_bezier.py index 0e61370..0a04a0f 100644 --- a/test/test_bezier.py +++ b/test/test_bezier.py @@ -1,8 +1,8 @@ from __future__ import division, absolute_import, print_function import numpy as np import unittest -from svgpathtools.bezier import bezier_point, bezier2polynomial, polynomial2bezier -from svgpathtools.path import bpoints2bezier +from svgpathtools.bezier import bezier_point, bezier2polynomial, polynomial2bezier, bezier_bounding_box, bezier_real_minmax +from svgpathtools.path import bpoints2bezier, CubicBezier class HigherOrderBezier: @@ -54,5 +54,53 @@ def test_polynomial2bezier(self): self.assertAlmostEqual(b.point(t), p(t), msg=msg) +class TestBezierBoundingBox(unittest.TestCase): + def test_bezier_bounding_box(self): + # This bezier curve has denominator == 0 but due to floating point arithmetic error it is not exactly 0 + zero_denominator_bezier_curve = CubicBezier(612.547 + 109.3261j, 579.967 - 19.4422j, 428.0344 - 19.4422j, 395.4374 + 109.3261j) + zero_denom_xmin, zero_denom_xmax, zero_denom_ymin, zero_denom_ymax = bezier_bounding_box(zero_denominator_bezier_curve) + self.assertAlmostEqual(zero_denom_xmin, 395.437400, 5) + self.assertAlmostEqual(zero_denom_xmax, 612.547, 5) + self.assertAlmostEqual(zero_denom_ymin, 12.7498749, 5) + self.assertAlmostEqual(zero_denom_ymax, 109.3261, 5) + + # This bezier curve has global extrema at the start and end points + start_end_bbox_bezier_curve = CubicBezier(886.8238 + 354.8439j, 884.4765 + 340.5983j, 877.6258 + 330.0518j, 868.2909 + 323.2453j) + start_end_xmin, start_end_xmax, start_end_ymin, start_end_ymax = bezier_bounding_box(start_end_bbox_bezier_curve) + self.assertAlmostEqual(start_end_xmin, 868.2909, 5) + self.assertAlmostEqual(start_end_xmax, 886.8238, 5) + self.assertAlmostEqual(start_end_ymin, 323.2453, 5) + self.assertAlmostEqual(start_end_ymax, 354.8439, 5) + + # This bezier curve is to cover some random case where at least one of the global extrema is not the start or end point + general_bezier_curve = CubicBezier(295.2282 + 402.0233j, 310.3734 + 355.5329j, 343.547 + 340.5983j, 390.122 + 355.7018j) + general_xmin, general_xmax, general_ymin, general_ymax = bezier_bounding_box(general_bezier_curve) + self.assertAlmostEqual(general_xmin, 295.2282, 5) + self.assertAlmostEqual(general_xmax, 390.121999999, 5) + self.assertAlmostEqual(general_ymin, 350.030030142, 5) + self.assertAlmostEqual(general_ymax, 402.0233, 5) + + +class TestBezierRealMinMax(unittest.TestCase): + def test_bezier_real_minmax(self): + # This bezier curve has denominator == 0 but due to floating point arithmetic error it is not exactly 0 + zero_denominator_bezier_curve = [109.3261, -19.4422, -19.4422, 109.3261] + zero_denominator_minmax = bezier_real_minmax(zero_denominator_bezier_curve) + self.assertAlmostEqual(zero_denominator_minmax[0], 12.7498749, 5) + self.assertAlmostEqual(zero_denominator_minmax[1], 109.3261, 5) + + # This bezier curve has global extrema at the start and end points + start_end_bbox_bezier_curve = [354.8439, 340.5983, 330.0518, 323.2453] + start_end_bbox_minmax = bezier_real_minmax(start_end_bbox_bezier_curve) + self.assertAlmostEqual(start_end_bbox_minmax[0], 323.2453, 5) + self.assertAlmostEqual(start_end_bbox_minmax[1], 354.8439, 5) + + # This bezier curve is to cover some random case where at least one of the global extrema is not the start or end point + general_bezier_curve = [402.0233, 355.5329, 340.5983, 355.7018] + general_minmax = bezier_real_minmax(general_bezier_curve) + self.assertAlmostEqual(general_minmax[0], 350.030030142, 5) + self.assertAlmostEqual(general_minmax[1], 402.0233, 5) + + if __name__ == '__main__': unittest.main() From 26337638fbc05c667d4557503c613a32115a7528 Mon Sep 17 00:00:00 2001 From: Bouhek Date: Thu, 1 Feb 2024 15:29:57 +0100 Subject: [PATCH 2/2] Fix lint --- svgpathtools/bezier.py | 1 + svgpathtools/constants.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/svgpathtools/bezier.py b/svgpathtools/bezier.py index 6b03602..32ca71f 100644 --- a/svgpathtools/bezier.py +++ b/svgpathtools/bezier.py @@ -12,6 +12,7 @@ from .polytools import real, imag, polyroots, polyroots01 from .constants import FLOAT_EPSILON + # Evaluation ################################################################## def n_choose_k(n, k): diff --git a/svgpathtools/constants.py b/svgpathtools/constants.py index 426fc22..4a623e3 100644 --- a/svgpathtools/constants.py +++ b/svgpathtools/constants.py @@ -1,3 +1,3 @@ """This submodule contains constants used throughout the project.""" -FLOAT_EPSILON = 1e-12 \ No newline at end of file +FLOAT_EPSILON = 1e-12