Repository: meerk40t/svgelements

Issue:
Cubic Bezier incorrect bounding box
This may be related to #186, but thought I'd bring it up in case it isn't known about. The computed bounding box of certain cubic beziers seems to be incorrect sometimes. Here's an example:
```python
from svgelements import CubicBezier

cubic = CubicBezier(0, -2-3j, -1-4j, -3j)
bbox = cubic.bbox()
print(bbox)  # outputs (-1.1547005383792515, -3.0, 0.0, 0.0)
```
This outputs `(-1.1547005383792515, -3.0, 0.0, 0.0)`, but `ymin` should really be lower than `-3`. Here is a picture of the bezier with the incorrectly computed bounding box:
![bad-bbox](https://user-images.githubusercontent.com/37314443/217055876-2ba213d4-03e0-4206-882e-4e58ab04895a.png)

svgelements version: 1.9.0
Python version: 3.8.1
OS: Windows

Discussion:

Patch:
diff --git a/svgelements/svgelements.py b/svgelements/svgelements.py
index 45a0336..e89a99d 100644
--- a/svgelements/svgelements.py
+++ b/svgelements/svgelements.py
@@ -4787,7 +4787,12 @@ class CubicBezier(Curve):
                 if 0 < r2 < 1:
                     local_extremizers.append(r2)
         else:
-            local_extremizers.append(0.5)
+            c = a[1] - a[0]
+            b = 2 * (a[0] - 2*a[1] + a[2])
+            if b != 0:
+                r0 = -c/b
+                if 0 < r0 < 1:
+                    local_extremizers.append(r0)
         local_extrema = [self.point(t)[v] for t in local_extremizers]
         return min(local_extrema), max(local_extrema)
 


Test patch:
diff --git a/test/test_cubic_bezier.py b/test/test_cubic_bezier.py
index e63893a..37ef038 100644
--- a/test/test_cubic_bezier.py
+++ b/test/test_cubic_bezier.py
@@ -1,3 +1,4 @@
+import random
 import unittest
 from random import *
 
@@ -5,12 +6,15 @@ from svgelements import *
 
 
 def get_random_cubic_bezier():
-    return CubicBezier((random() * 50, random() * 50), (random() * 50, random() * 50),
-                       (random() * 50, random() * 50), (random() * 50, random() * 50))
+    return CubicBezier(
+        (random() * 50, random() * 50),
+        (random() * 50, random() * 50),
+        (random() * 50, random() * 50),
+        (random() * 50, random() * 50),
+    )
 
 
 class TestElementCubicBezierLength(unittest.TestCase):
-
     def test_cubic_bezier_length(self):
         n = 100
         error = 0
@@ -25,18 +29,20 @@ class TestElementCubicBezierLength(unittest.TestCase):
 
 
 class TestElementCubicBezierPoint(unittest.TestCase):
-
     def test_cubic_bezier_point_start_stop(self):
         import numpy as np
+
         for _ in range(1000):
             b = get_random_cubic_bezier()
             self.assertEqual(b.start, b.point(0))
             self.assertEqual(b.end, b.point(1))
-            self.assertTrue(np.all(np.array([list(b.start), list(b.end)])
-                                   == b.npoint([0, 1])))
+            self.assertTrue(
+                np.all(np.array([list(b.start), list(b.end)]) == b.npoint([0, 1]))
+            )
 
     def test_cubic_bezier_point_implementations_match(self):
         import numpy as np
+
         for _ in range(1000):
             b = get_random_cubic_bezier()
 
@@ -50,3 +56,21 @@ class TestElementCubicBezierPoint(unittest.TestCase):
             for p, p1, p2 in zip(pos, v1, v2):
                 self.assertEqual(b.point(p), Point(p1))
                 self.assertEqual(Point(p1), Point(p2))
+
+    def test_cubic_bounds_issue_214(self):
+        cubic = CubicBezier(0, -2 - 3j, -1 - 4j, -3j)
+        bbox = cubic.bbox()
+        self.assertLess(bbox[1], -3)
+
+    def test_cubic_bounds_issue_214_random(self):
+        for i in range(100):
+            a = random() * 5
+            b = random() * 5
+            c = random() * 5
+            d = a - 3 * b + 3 * c
+            cubic1 = CubicBezier(a, b, c, d)
+            bbox1 = cubic1.bbox()
+            cubic2 = CubicBezier(a, b, c, d + 1e-11)
+            bbox2 = cubic2.bbox()
+            for a, b in zip(bbox1, bbox2):
+                self.assertAlmostEqual(a, b, delta=1e-5)