diff --git a/pyonfx/shape.py b/pyonfx/shape.py index fa36d2ab..38934703 100644 --- a/pyonfx/shape.py +++ b/pyonfx/shape.py @@ -247,9 +247,9 @@ def bounding(self, exact: bool = True) -> Tuple[float, float, float, float]: Examples: .. code-block:: python3 - print("Left-top: %d %d\\nRight-bottom: %d %d" % (Shape("m 10 5 l 25 5 25 42 10 42").bounding())) - print(Shape("m 313 312 b 254 287 482 38 277 212 l 436 269 b 378 388 461 671 260 481").bounding()) - print(Shape("m 313 312 b 254 287 482 38 277 212 l 436 269 b 378 388 461 671 260 481").bounding(exact=False)) + print( "Left-top: %d %d\\nRight-bottom: %d %d" % ( Shape("m 10 5 l 25 5 25 42 10 42").bounding() ) ) + print( Shape("m 313 312 b 254 287 482 38 277 212 l 436 269 b 378 388 461 671 260 481").bounding() ) + print( Shape("m 313 312 b 254 287 482 38 277 212 l 436 269 b 378 388 461 671 260 481").bounding(exact=False) ) >>> Left-top: 10 5 >>> Right-bottom: 25 42 @@ -426,11 +426,10 @@ def update_min_max(x, y): return x_min, y_min, x_max, y_max - def move(self, x: float = None, y: float = None) -> Shape: + def move(self, x: float, y: float) -> Shape: """Moves shape coordinates in given direction. - | If neither x and y are passed, it will automatically center the shape to the origin (0,0). - | This function is an high level function, it just uses Shape.map, which is more advanced. Additionally, it is an easy way to center a shape. + | This function is a high level function, it just uses Shape.map, which is more advanced. Parameters: x (int or float): Displacement along the x-axis. @@ -446,17 +445,92 @@ def move(self, x: float = None, y: float = None) -> Shape: >>> m -5 10 l 25 10 25 30 -5 30 """ - if x is None and y is None: - x, y = [-1 * el for el in self.bounding()[0:2]] - elif x is None: - x = 0 - elif y is None: - y = 0 - # Update shape self.map(lambda cx, cy: (cx + x, cy + y)) return self + def align(self, anchor: int = 5, an: int = 5) -> Shape: + """Aligns the shape. + + | If no argument for anchor is passed, it will automatically center the shape. + + Parameters: + anchor (int): The shape's position relative to the position anchor. If anchor=7, the position anchor would be at the top left of the shape's bounding box. If anchor=5, it would be in the center. If anchor=3, it would be at the bottom right. + an (int): Alignment used for the shape (e.g. for {\\\\an8} you would use an=8). + + Returns: + A pointer to the current object. + + Examples: + .. code-block:: python3 + + print( Shape("m 10 10 l 30 10 30 20 10 20").align() ) + + >>> m 0 0 l 20 0 20 10 0 10 + """ + + y_axis_anchor, x_axis_anchor = divmod(anchor - 1, 3) + y_axis_an, x_axis_an = divmod(an - 1, 3) + x_min, y_min, x_max, y_max = self.bounding() + libass_boundings = self.bounding(exact=False) + x_min_libass, y_min_libass, x_max_libass, y_max_libass = libass_boundings + x_move = -x_min + y_move = -y_min + + # Center shape along x-axis + if x_axis_an == 0: # left + # center shape + x_move -= (x_max - x_min) / 2 + elif x_axis_an == 1: # center + # adjust for imprecise calculation from libass (when using bezier curves) + x_move -= (x_max - x_min) / 2 - (x_max_libass - x_min_libass) / 2 + elif x_axis_an == 2: # right + # center shape + x_move += (x_max - x_min) / 2 + # adjust for imprecise calculation from libass (when using bezier curves) + x_move -= (x_max - x_min) - (x_max_libass - x_min_libass) + else: + raise ValueError("an must be an integer from 1 to 9") + + # Center shape along y-axis + if y_axis_an == 0: # bottom + # center shape + y_move += (y_max - y_min) / 2 + # adjust for imprecise calculation from libass (when using bezier curves) + y_move -= (y_max - y_min) - (y_max_libass - y_min_libass) + elif y_axis_an == 1: # middle + # adjust for imprecise calculation from libass (when using bezier curves) + y_move -= (y_max - y_min) / 2 - (y_max_libass - y_min_libass) / 2 + elif y_axis_an == 2: # top + # center shape + y_move -= (y_max - y_min) / 2 + else: + raise ValueError("an must be an integer from 1 to 9") + + # Set anchor along x-axis + if x_axis_anchor == 0: # left + x_move += (x_max - x_min) / 2 + elif x_axis_anchor == 1: # center + pass + elif x_axis_anchor == 2: # right + x_move -= (x_max - x_min) / 2 + else: + raise ValueError("anchor must be an integer from 1 to 9") + + # Set anchor along y-axis + if y_axis_anchor == 0: # bottom + y_move -= (y_max - y_min) / 2 + elif y_axis_anchor == 1: # middle + pass + elif y_axis_anchor == 2: # top + y_move += (y_max - y_min) / 2 + else: + raise ValueError("anchor must be an integer from 1 to 9") + + # Update shape + self.map(lambda cx, cy: (cx + x_move, cy + y_move)) + return self + def flatten(self, tolerance: float = 1.0) -> Shape: """Splits shape's bezier curves into lines. @@ -1068,7 +1142,7 @@ def rotate_on_axis_z(point, theta): shape = Shape(" ".join(shape)) # Return result centered - return shape.move() + return shape.align() @staticmethod def star(edges: int, inner_size: float, outer_size: float) -> Shape: diff --git a/tests/test_shape.py b/tests/test_shape.py index 1f3fd136..77e047ea 100644 --- a/tests/test_shape.py +++ b/tests/test_shape.py @@ -1,5 +1,6 @@ import pytest from pyonfx import * +from copy import copy def test_transform(): @@ -76,9 +77,96 @@ def test_move(): dest = Shape("m -95.5 -2 l 105 -2 b 105 98 -95 98 -95.5 -2 c") assert original.move(5, -2) == dest + +def test_align(): + # Test centering original = Shape("m 0 0 l 20 0 20 10 0 10") - assert original.move() == original - assert original.move(10, 400).move() == original + assert copy(original).align() == original + assert copy(original).move(10, 400).align() == original + + # Test an + original = Shape("m 0 0 l 500 0 500 200 0 200") + assert copy(original).align(anchor=5, an=5) == original + dest1 = Shape("m -250 100 l 250 100 250 300 -250 300") + assert original.align(anchor=5, an=1) == dest1 + dest2 = Shape("m 0 100 l 500 100 500 300 0 300") + assert original.align(anchor=5, an=2) == dest2 + dest3 = Shape("m 250 100 l 750 100 750 300 250 300") + assert original.align(anchor=5, an=3) == dest3 + dest4 = Shape("m -250 0 l 250 0 250 200 -250 200") + assert original.align(anchor=5, an=4) == dest4 + dest6 = Shape("m 250 0 l 750 0 750 200 250 200") + assert original.align(anchor=5, an=6) == dest6 + dest7 = Shape("m -250 -100 l 250 -100 250 100 -250 100") + assert original.align(anchor=5, an=7) == dest7 + dest8 = Shape("m 0 -100 l 500 -100 500 100 0 100") + assert original.align(anchor=5, an=8) == dest8 + dest9 = Shape("m 250 -100 l 750 -100 750 100 250 100") + assert original.align(anchor=5, an=9) == dest9 + + original = Shape( + "m 411.87 306.36 b 385.63 228.63 445.78 147.2 536.77 144.41 630.18 147.77 697 236.33 665.81 310.49 591.86 453.18 437.07 395.59 416 316.68" + ) + dest3 = Shape( + "m 183.614 344.04 b 157.374 266.31 217.524 184.88 308.514 182.09 401.924 185.45 468.744 274.01 437.554 348.17 363.604 490.86 208.814 433.27 187.744 354.36" + ) + assert original.align(anchor=5, an=3) == dest3 + dest5 = Shape( + "m 27.929 189.655 b 1.689 111.925 61.839 30.495 152.829 27.705 246.239 31.065 313.059 119.625 281.869 193.785 207.919 336.475 53.129 278.885 32.059 199.975" + ) + assert original.align(anchor=5, an=5) == dest5 + dest7 = Shape( + "m -127.756 35.27 b -153.996 -42.46 -93.846 -123.89 -2.856 -126.68 90.554 -123.32 157.374 -34.76 126.184 39.4 52.234 182.09 -102.556 124.5 -123.626 45.59" + ) + assert original.align(anchor=5, an=7) == dest7 + + # Test anchor + original = Shape("m 0 0 l 500 0 500 200 0 200") + dest1 = Shape("m 250 -100 l 750 -100 750 100 250 100") + assert original.align(anchor=1, an=5) == dest1 + dest2 = Shape("m 0 -100 l 500 -100 500 100 0 100") + assert original.align(anchor=2, an=5) == dest2 + dest3 = Shape("m -250 -100 l 250 -100 250 100 -250 100") + assert original.align(anchor=3, an=5) == dest3 + dest4 = Shape("m 250 0 l 750 0 750 200 250 200") + assert original.align(anchor=4, an=5) == dest4 + dest5 = Shape("m 0 0 l 500 0 500 200 0 200") + assert original.align(anchor=5, an=5) == dest5 + dest6 = Shape("m -250 0 l 250 0 250 200 -250 200") + assert original.align(anchor=6, an=5) == dest6 + dest7 = Shape("m 250 100 l 750 100 750 300 250 300") + assert original.align(anchor=7, an=5) == dest7 + dest8 = Shape("m 0 100 l 500 100 500 300 0 300") + assert original.align(anchor=8, an=5) == dest8 + dest9 = Shape("m -250 100 l 250 100 250 300 -250 300") + assert original.align(anchor=9, an=5) == dest9 + + # Test anchor + an + original = Shape("m 342 352 l 338 544 734 536 736 350 b 784 320 1157 167 930 232") + dest_anchor_7_an_5 = Shape( + "m 413.5 324.427 l 409.5 516.427 805.5 508.427 807.5 322.427 b 855.5 292.427 1228.5 139.427 1001.5 204.427" + ) + assert original.align(anchor=7, an=5) == dest_anchor_7_an_5 + dest_anchor_9_an_1 = Shape( + "m -660.664 512.927 l -664.664 704.927 -268.664 696.927 -266.664 510.927 b -218.664 480.927 154.336 327.927 -72.664 392.927" + ) + assert original.align(anchor=9, an=1) == dest_anchor_9_an_1 + dest_anchor_9_an_5 = Shape( + "m -251.164 324.427 l -255.164 516.427 140.836 508.427 142.836 322.427 b 190.836 292.427 563.836 139.427 336.836 204.427" + ) + assert original.align(anchor=9, an=5) == dest_anchor_9_an_5 + dest_anchor_9_an_9 = Shape( + "m 158.336 135.927 l 154.336 327.927 550.336 319.927 552.336 133.927 b 600.336 103.927 973.336 -49.073 746.336 15.927" + ) + assert original.align(anchor=9, an=9) == dest_anchor_9_an_9 + dest_anchor_3_an_5 = Shape( + "m -251.164 -3.5 l -255.164 188.5 140.836 180.5 142.836 -5.5 b 190.836 -35.5 563.836 -188.5 336.836 -123.5" + ) + assert original.align(anchor=3, an=5) == dest_anchor_3_an_5 + dest_anchor_5_an_5 = Shape( + "m 81.168 160.464 l 77.168 352.464 473.168 344.464 475.168 158.464 b 523.168 128.464 896.168 -24.536 669.168 40.464" + ) + assert original.align(anchor=5, an=5) == dest_anchor_5_an_5 def test_flatten():