boolean_ops.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. from __future__ import annotations
  2. import numpy as np
  3. import pathops
  4. from manimlib.mobject.types.vectorized_mobject import VMobject
  5. # Boolean operations between 2D mobjects
  6. # Borrowed from from https://github.com/ManimCommunity/manim/
  7. def _convert_vmobject_to_skia_path(vmobject: VMobject) -> pathops.Path:
  8. path = pathops.Path()
  9. for submob in vmobject.family_members_with_points():
  10. for subpath in submob.get_subpaths():
  11. quads = vmobject.get_bezier_tuples_from_points(subpath)
  12. start = subpath[0]
  13. path.moveTo(*start[:2])
  14. for p0, p1, p2 in quads:
  15. path.quadTo(*p1[:2], *p2[:2])
  16. if vmobject.consider_points_equal(subpath[0], subpath[-1]):
  17. path.close()
  18. return path
  19. def _convert_skia_path_to_vmobject(
  20. path: pathops.Path,
  21. vmobject: VMobject
  22. ) -> VMobject:
  23. PathVerb = pathops.PathVerb
  24. current_path_start = np.array([0.0, 0.0, 0.0])
  25. for path_verb, points in path:
  26. if path_verb == PathVerb.CLOSE:
  27. vmobject.add_line_to(current_path_start)
  28. else:
  29. points = np.hstack((np.array(points), np.zeros((len(points), 1))))
  30. if path_verb == PathVerb.MOVE:
  31. for point in points:
  32. current_path_start = point
  33. vmobject.start_new_path(point)
  34. elif path_verb == PathVerb.CUBIC:
  35. vmobject.add_cubic_bezier_curve_to(*points)
  36. elif path_verb == PathVerb.LINE:
  37. vmobject.add_line_to(points[0])
  38. elif path_verb == PathVerb.QUAD:
  39. vmobject.add_quadratic_bezier_curve_to(*points)
  40. else:
  41. raise Exception(f"Unsupported: {path_verb}")
  42. return vmobject.reverse_points()
  43. class Union(VMobject):
  44. def __init__(self, *vmobjects: VMobject, **kwargs):
  45. if len(vmobjects) < 2:
  46. raise ValueError("At least 2 mobjects needed for Union.")
  47. super().__init__(**kwargs)
  48. outpen = pathops.Path()
  49. paths = [
  50. _convert_vmobject_to_skia_path(vmobject)
  51. for vmobject in vmobjects
  52. ]
  53. pathops.union(paths, outpen.getPen())
  54. _convert_skia_path_to_vmobject(outpen, self)
  55. class Difference(VMobject):
  56. def __init__(self, subject: VMobject, clip: VMobject, **kwargs):
  57. super().__init__(**kwargs)
  58. outpen = pathops.Path()
  59. pathops.difference(
  60. [_convert_vmobject_to_skia_path(subject)],
  61. [_convert_vmobject_to_skia_path(clip)],
  62. outpen.getPen(),
  63. )
  64. _convert_skia_path_to_vmobject(outpen, self)
  65. class Intersection(VMobject):
  66. def __init__(self, *vmobjects: VMobject, **kwargs):
  67. if len(vmobjects) < 2:
  68. raise ValueError("At least 2 mobjects needed for Intersection.")
  69. super().__init__(**kwargs)
  70. outpen = pathops.Path()
  71. pathops.intersection(
  72. [_convert_vmobject_to_skia_path(vmobjects[0])],
  73. [_convert_vmobject_to_skia_path(vmobjects[1])],
  74. outpen.getPen(),
  75. )
  76. new_outpen = outpen
  77. for _i in range(2, len(vmobjects)):
  78. new_outpen = pathops.Path()
  79. pathops.intersection(
  80. [outpen],
  81. [_convert_vmobject_to_skia_path(vmobjects[_i])],
  82. new_outpen.getPen(),
  83. )
  84. outpen = new_outpen
  85. _convert_skia_path_to_vmobject(outpen, self)
  86. class Exclusion(VMobject):
  87. def __init__(self, *vmobjects: VMobject, **kwargs):
  88. if len(vmobjects) < 2:
  89. raise ValueError("At least 2 mobjects needed for Exclusion.")
  90. super().__init__(**kwargs)
  91. outpen = pathops.Path()
  92. pathops.xor(
  93. [_convert_vmobject_to_skia_path(vmobjects[0])],
  94. [_convert_vmobject_to_skia_path(vmobjects[1])],
  95. outpen.getPen(),
  96. )
  97. new_outpen = outpen
  98. for _i in range(2, len(vmobjects)):
  99. new_outpen = pathops.Path()
  100. pathops.xor(
  101. [outpen],
  102. [_convert_vmobject_to_skia_path(vmobjects[_i])],
  103. new_outpen.getPen(),
  104. )
  105. outpen = new_outpen
  106. _convert_skia_path_to_vmobject(outpen, self)