From 818e6825bf8f2703f44ff911ce48aab04e02c141 Mon Sep 17 00:00:00 2001 From: tatarize Date: Wed, 17 Jun 2020 00:39:34 -0900 Subject: [PATCH] Corrections for Path Bounding Boxes. --- svgelements/svgelements.py | 104 ++++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 12 deletions(-) diff --git a/svgelements/svgelements.py b/svgelements/svgelements.py index 1393f5ed..4a5152e7 100644 --- a/svgelements/svgelements.py +++ b/svgelements/svgelements.py @@ -3304,6 +3304,32 @@ def point(self, position): y = (1 - position) * (1 - position) * y0 + 2 * (1 - position) * position * y1 + position * position * y2 return Point(x, y) + def bbox(self): + """ + Returns the bounding box for the quadratic bezier curve. + """ + n = self.start[0] - self.control[0] + d = self.start[0] - 2 * self.control[0] + self.end[0] + if d != 0: + t = n / d + else: + t = 0.5 + if 0 < t < 1: + x_values = [self.start[0], self.end[0], self.point(t)[0]] + else: + x_values = [self.start[0], self.end[0]] + n = self.start[1] - self.control[1] + d = self.start[1] - 2 * self.control[1] + self.end[1] + if d != 0: + t = n / d + else: + t = 0.5 + if 0 < t < 1: + y_values = [self.start[1], self.end[1], self.point(t)[1]] + else: + y_values = [self.start[1], self.end[1]] + return min(x_values), min(y_values), max(x_values), max(y_values) + def length(self, error=None, min_depth=None): """Calculate the length of the path up to a certain position""" a = self.start - 2 * self.control + self.end @@ -3443,6 +3469,42 @@ def point(self, position): position * position * position * y3 return Point(x, y) + def bbox(self): + """returns the tight fitting bounding box of the bezier curve. + Code by: + https://github.com/mathandy/svgpathtools + """ + xmin, xmax = self._real_minmax(0) + ymin, ymax = self._real_minmax(1) + return xmin, ymin, xmax, ymax + + def _real_minmax(self, v): + """returns the minimum and maximum for a real cubic bezier, with a non-zero denom + Code by: + https://github.com/mathandy/svgpathtools + """ + local_extremizers = [0, 1] + a = [c[v] for c in self] + denom = a[0] - 3 * a[1] + 3 * a[2] - a[3] + if abs(denom) >= 1e-12: + 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) + tau = a[0] - 2 * a[1] + a[2] + r1 = (tau + sqdelta) / denom + r2 = (tau - sqdelta) / denom + if 0 < r1 < 1: + local_extremizers.append(r1) + if 0 < r2 < 1: + local_extremizers.append(r2) + else: + local_extremizers.append(0.5) + local_extrema = [self.point(t)[v] for t in local_extremizers] + return min(local_extrema), max(local_extrema) + def length(self, error=ERROR, min_depth=MIN_DEPTH): """Calculate the length of the path up to a certain position""" return self._line_length(0, 1, error, min_depth) @@ -4133,18 +4195,36 @@ def get_ellipse(self): return Ellipse(self.center, self.rx, self.ry, self.get_rotation()) def bbox(self): - """Returns the bounding box of the arc.""" - # TODO: truncated the bounding box to the arc rather than the entire ellipse. - theta = Point.angle(self.center, self.prx) - a = Point.distance(self.center, self.prx) - b = Point.distance(self.center, self.pry) - cos_theta = cos(theta) - sin_theta = sin(theta) - xmax = sqrt(a * a * cos_theta * cos_theta + b * b * sin_theta * sin_theta) - xmin = -xmax - ymax = sqrt(a * a * sin_theta * sin_theta + b * b * cos_theta * cos_theta) - ymin = -xmax - return xmin + self.center[0], ymin + self.center[1], xmax + self.center[0], ymax + self.center[1] + """Find the bounding box of a arc. + Code from: https://github.com/mathandy/svgpathtools + """ + phi = self.get_rotation().as_radians + if cos(phi) == 0: + atan_x = pi / 2 + atan_y = 0 + elif sin(phi) == 0: + atan_x = 0 + atan_y = pi / 2 + else: + rx, ry = self.rx, self.ry + atan_x = atan(-(ry / rx) * tan(phi)) + atan_y = atan((ry / rx) / tan(phi)) + + def angle_inv(ang, k): # inverse of angle from Arc.derivative() + return ((ang + pi * k) * (360 / (2 * pi)) - self.theta) / self.delta + + xtrema = [self.start[0], self.end[0]] + ytrema = [self.start[1], self.end[1]] + + for k in range(-4, 5): + tx = angle_inv(atan_x, k) + ty = angle_inv(atan_y, k) + if 0 <= tx <= 1: + xtrema.append(self.point(tx)[0]) + if 0 <= ty <= 1: + ytrema.append(self.point(ty)[1]) + + return min(xtrema), min(ytrema), max(xtrema), max(ytrema) def d(self, current_point=None, smooth=False): if current_point is None: