diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index d83c3d2d..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ -### If this is a support request: - -**Please attempt to solve the problem on your own before opening an issue.** -Between old issues, StackOverflow, and Google, you should be able to find -solutions to most of the common problems. - -Include at least: -1. Steps to reproduce the issue (e.g. the command you ran) -2. The unexpected behavior that occurred (e.g. error messages or screenshots) -3. The environment (e.g. operating system and version of manim) - - -### If this is a feature request: -Include the motivation for making this change. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..bad3125e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,20 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +### Describe the bug + + +**Code**: + + +**Wrong display or Error traceback**: + + +### Additional context + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..5581a02a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Ask A Question + url: https://github.com/3b1b/manim/discussions/categories/q-a + about: Please ask questions you encountered here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/error-when-using.md b/.github/ISSUE_TEMPLATE/error-when-using.md new file mode 100644 index 00000000..0686dfd3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/error-when-using.md @@ -0,0 +1,23 @@ +--- +name: Error when using +about: The error you encountered while using manim +title: '' +labels: '' +assignees: '' + +--- + +### Describe the error + + +### Code and Error +**Code**: + + +**Error**: + + +### Environment +**OS System**: +**manim version**: master +**python version**: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cd810c78..3ca8f474 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,18 @@ -Thanks for contributing to manim! + -**Please ensure that your pull request works with the latest version of manim.** -You should also include: +## Motivation + -1. The motivation for making this change (or link the relevant issues) -2. How you tested the new behavior (e.g. a minimal working example, before/after -screenshots, gifs, commands, etc.) This is rather informal at the moment, but -the goal is to show us how you know the pull request works as intended. +## Proposed changes + +- +- +- + +## Test + +**Code**: + +**Result**: \ No newline at end of file diff --git a/example_scenes.py b/example_scenes.py index d8b7d4b2..f2a95727 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -497,7 +497,8 @@ class ControlsExample(Scene): self.add(MotionMobject(text)) self.textbox.set_value("Manim") - self.embed() + # self.wait(60) + # self.embed() # See https://github.com/3b1b/videos for many, many more diff --git a/manimlib/event_handler/__init__.py b/manimlib/event_handler/__init__.py new file mode 100644 index 00000000..c6c25536 --- /dev/null +++ b/manimlib/event_handler/__init__.py @@ -0,0 +1,6 @@ +from manimlib.event_handler.event_dispatcher import EventDispatcher + + +# This is supposed to be a Singleton +# i.e., during runtime there should be only one object of Event Dispatcher +EVENT_DISPATCHER = EventDispatcher() diff --git a/manimlib/event_handler/event_dispatcher.py b/manimlib/event_handler/event_dispatcher.py new file mode 100644 index 00000000..a760d9ee --- /dev/null +++ b/manimlib/event_handler/event_dispatcher.py @@ -0,0 +1,92 @@ +import numpy as np + +from manimlib.event_handler.event_type import EventType +from manimlib.event_handler.event_listner import EventListner + + +class EventDispatcher(object): + def __init__(self): + self.event_listners = { + event_type: [] + for event_type in EventType + } + self.mouse_point = np.array((0., 0., 0.)) + self.mouse_drag_point = np.array((0., 0., 0.)) + self.pressed_keys = set() + self.draggable_object_listners = [] + + def add_listner(self, event_listner): + assert(isinstance(event_listner, EventListner)) + self.event_listners[event_listner.event_type].append(event_listner) + return self + + def remove_listner(self, event_listner): + assert(isinstance(event_listner, EventListner)) + try: + while event_listner in self.event_listners[event_listner.event_type]: + self.event_listners[event_listner.event_type].remove(event_listner) + except: + # raise ValueError("Handler is not handling this event, so cannot remove it.") + pass + return self + + def dispatch(self, event_type, **event_data): + + if event_type == EventType.MouseMotionEvent: + self.mouse_point = event_data["point"] + elif event_type == EventType.MouseDragEvent: + self.mouse_drag_point = event_data["point"] + elif event_type == EventType.KeyPressEvent: + self.pressed_keys.add(event_data["symbol"]) # Modifiers? + elif event_type == EventType.KeyReleaseEvent: + self.pressed_keys.difference_update({event_data["symbol"]}) # Modifiers? + elif event_type == EventType.MousePressEvent: + self.draggable_object_listners = [ + listner + for listner in self.event_listners[EventType.MouseDragEvent] + if listner.mobject.is_point_touching(self.mouse_point) + ] + elif event_type == EventType.MouseReleaseEvent: + self.draggable_object_listners = [] + + propagate_event = None + + if event_type == EventType.MouseDragEvent: + for listner in self.draggable_object_listners: + assert(isinstance(listner, EventListner)) + propagate_event = listner.callback(listner.mobject, event_data) + if propagate_event is not None and propagate_event is False: + return propagate_event + + elif event_type.value.startswith('mouse'): + for listner in self.event_listners[event_type]: + if listner.mobject.is_point_touching(self.mouse_point): + propagate_event = listner.callback( + listner.mobject, event_data) + if propagate_event is not None and propagate_event is False: + return propagate_event + + elif event_type.value.startswith('key'): + for listner in self.event_listners[event_type]: + propagate_event = listner.callback(listner.mobject, event_data) + if propagate_event is not None and propagate_event is False: + return propagate_event + + return propagate_event + + def get_listners_count(self): + return sum([len(value) for key, value in self.event_listners.items()]) + + def get_mouse_point(self): + return self.mouse_point + + def get_mouse_drag_point(self): + return self.mouse_drag_point + + def is_key_pressed(self, symbol): + return (symbol in self.pressed_keys) + + __iadd__ = add_listner + __isub__ = remove_listner + __call__ = dispatch + __len__ = get_listners_count diff --git a/manimlib/event_handler/event_listner.py b/manimlib/event_handler/event_listner.py new file mode 100644 index 00000000..2f8663f7 --- /dev/null +++ b/manimlib/event_handler/event_listner.py @@ -0,0 +1,15 @@ +class EventListner(object): + def __init__(self, mobject, event_type, event_callback): + self.mobject = mobject + self.event_type = event_type + self.callback = event_callback + + def __eq__(self, o: object) -> bool: + return_val = False + try: + return_val = self.callback == o.callback \ + and self.mobject == o.mobject \ + and self.event_type == o.event_type + except: + pass + return return_val diff --git a/manimlib/event_handler/event_type.py b/manimlib/event_handler/event_type.py new file mode 100644 index 00000000..6cd9f73e --- /dev/null +++ b/manimlib/event_handler/event_type.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class EventType(Enum): + MouseMotionEvent = 'mouse_motion_event' + MousePressEvent = 'mouse_press_event' + MouseReleaseEvent = 'mouse_release_event' + MouseDragEvent = 'mouse_drag_event' + MouseScrollEvent = 'mouse_scroll_event' + KeyPressEvent = 'key_press_event' + KeyReleaseEvent = 'key_release_event' diff --git a/manimlib/mobject/interactive.py b/manimlib/mobject/interactive.py index 7e72d816..df9e0432 100644 --- a/manimlib/mobject/interactive.py +++ b/manimlib/mobject/interactive.py @@ -22,50 +22,43 @@ class MotionMobject(Mobject): You could hold and drag this object to any position """ - CONFIG = { - "listen_to_events": True - } - def __init__(self, mobject, **kwargs): super().__init__(**kwargs) + assert(isinstance(mobject, Mobject)) self.mobject = mobject + self.mobject.add_mouse_drag_listner(self.mob_on_mouse_drag) # To avoid locking it as static mobject self.mobject.add_updater(lambda mob: None) self.add(mobject) - def on_mouse_drag(self, point, d_point, buttons, modifiers): - if self.mobject.is_point_touching(point): - self.mobject.move_to(point) - return False + def mob_on_mouse_drag(self, mob, event_data): + mob.move_to(event_data["point"]) + return False class Button(Mobject): """ Pass any mobject and register an on_click method - """ - CONFIG = { - "listen_to_events": True - } + The on_click method takes mobject as argument like updater + """ def __init__(self, mobject, on_click, **kwargs): super().__init__(**kwargs) - self.mobject, self.on_click = mobject, on_click + assert(isinstance(mobject, Mobject)) + self.on_click = on_click + self.mobject = mobject + self.mobject.add_mouse_press_listner(self.mob_on_mouse_press) self.add(self.mobject) - def on_mouse_press(self, point, button, mods): - if self.mobject.is_point_touching(point): - self.on_click() - return False + def mob_on_mouse_press(self, mob, event_data): + self.on_click(mob) + return False # Controls -class ContolMobject(ValueTracker): - CONFIG = { - "listen_to_events": True - } - +class ControlMobject(ValueTracker): def __init__(self, value, *mobjects, **kwargs): super().__init__(value=value, **kwargs) self.add(*mobjects) @@ -88,7 +81,7 @@ class ContolMobject(ValueTracker): pass -class EnableDisableButton(ContolMobject): +class EnableDisableButton(ControlMobject): CONFIG = { "value_type": np.dtype(bool), "rect_kwargs": { @@ -104,6 +97,7 @@ class EnableDisableButton(ContolMobject): digest_config(self, kwargs) self.box = Rectangle(**self.rect_kwargs) super().__init__(value, self.box, **kwargs) + self.add_mouse_press_listner(self.on_mouse_press) def assert_value(self, value): assert(isinstance(value, bool)) @@ -117,12 +111,12 @@ class EnableDisableButton(ContolMobject): def toggle_value(self): super().set_value(not self.get_value()) - def on_mouse_press(self, point, button, mods): - self.toggle_value() + def on_mouse_press(self, mob, event_data): + mob.toggle_value() return False -class Checkbox(ContolMobject): +class Checkbox(ControlMobject): CONFIG = { "value_type": np.dtype(bool), "rect_kwargs": { @@ -147,6 +141,7 @@ class Checkbox(ContolMobject): self.box = Rectangle(**self.rect_kwargs) self.box_content = self.get_checkmark() if value else self.get_cross() super().__init__(value, self.box, self.box_content, **kwargs) + self.add_mouse_press_listner(self.on_mouse_press) def assert_value(self, value): assert(isinstance(value, bool)) @@ -160,8 +155,8 @@ class Checkbox(ContolMobject): else: self.box_content.become(self.get_cross()) - def on_mouse_press(self, point, button, mods): - self.toggle_value() + def on_mouse_press(self, mob, event_data): + mob.toggle_value() return False # Helper methods @@ -191,7 +186,7 @@ class Checkbox(ContolMobject): return cross -class LinearNumberSlider(ContolMobject): +class LinearNumberSlider(ControlMobject): CONFIG = { "value_type": np.float64, "min_value": -10.0, @@ -222,6 +217,8 @@ class LinearNumberSlider(ContolMobject): self.slider_axis.set_opacity(0.0) self.slider.move_to(self.slider_axis) + self.slider.add_mouse_drag_listner(self.slider_on_mouse_drag) + super().__init__(value, self.bar, self.slider, self.slider_axis, ** kwargs) def assert_value(self, value): @@ -231,10 +228,9 @@ class LinearNumberSlider(ContolMobject): prop = (value - self.min_value) / (self.max_value - self.min_value) self.slider.move_to(self.slider_axis.point_from_proportion(prop)) - def on_mouse_drag(self, point, d_point, buttons, modifiers): - if self.slider.is_point_touching(point): - self.set_value(self.get_value_from_point(point)) - return False + def slider_on_mouse_drag(self, mob, event_data): + self.set_value(self.get_value_from_point(event_data["point"])) + return False # Helper Methods @@ -348,7 +344,7 @@ class ColorSliders(Group): return rgba[3] -class Textbox(ContolMobject): +class Textbox(ControlMobject): CONFIG = { "value_type": np.dtype(object), @@ -371,10 +367,12 @@ class Textbox(ContolMobject): digest_config(self, kwargs) self.isActive = self.isInitiallyActive self.box = Rectangle(**self.box_kwargs) + self.box.add_mouse_press_listner(self.box_on_mouse_press) self.text = Text(value, **self.text_kwargs) super().__init__(value, self.box, self.text, **kwargs) self.update_text(value) self.active_anim(self.isActive) + self.add_key_press_listner(self.on_key_press) def set_value_anim(self, value): self.update_text(value) @@ -397,16 +395,17 @@ class Textbox(ContolMobject): else: self.box.set_stroke(self.deactive_color) - def on_mouse_press(self, point, button, mods): - if self.box.is_point_touching(point): - self.isActive = not self.isActive - self.active_anim(self.isActive) - return False + def box_on_mouse_press(self, mob, event_data): + self.isActive = not self.isActive + self.active_anim(self.isActive) + return False - def on_key_press(self, symbol, modifiers): + def on_key_press(self, mob, event_data): + symbol = event_data["symbol"] + modifiers = event_data["modifiers"] char = chr(symbol) - if self.isActive: - old_value = self.get_value() + if mob.isActive: + old_value = mob.get_value() new_value = old_value if char.isalnum(): if (modifiers & PygletWindowKeys.MOD_SHIFT) or (modifiers & PygletWindowKeys.MOD_CAPSLOCK): @@ -419,13 +418,12 @@ class Textbox(ContolMobject): new_value = old_value + '\t' elif symbol == PygletWindowKeys.BACKSPACE: new_value = old_value[:-1] or '' - self.set_value(new_value) + mob.set_value(new_value) return False class ControlPanel(Group): CONFIG = { - "listen_to_events": True, "panel_kwargs": { "width": FRAME_WIDTH / 4, "height": MED_SMALL_BUFF + FRAME_HEIGHT, @@ -451,6 +449,7 @@ class ControlPanel(Group): self.panel = Rectangle(**self.panel_kwargs) self.panel.to_corner(UP + LEFT, buff=0) self.panel.shift(self.panel.get_height() * UP) + self.panel.add_mouse_scroll_listner(self.panel_on_mouse_scroll) self.panel_opener_rect = Rectangle(**self.opener_kwargs) self.panel_info_text = Text(**self.opener_text_kwargs) @@ -458,6 +457,7 @@ class ControlPanel(Group): self.panel_opener = Group(self.panel_opener_rect, self.panel_info_text) self.panel_opener.next_to(self.panel, DOWN, aligned_edge=DOWN) + self.panel_opener.add_mouse_drag_listner(self.panel_opener_on_mouse_drag) self.controls = Group(*controls) self.controls.arrange(DOWN, center=False, aligned_edge=ORIGIN) @@ -510,14 +510,14 @@ class ControlPanel(Group): self.move_panel_and_controls_to_panel_opener() return self - def on_mouse_drag(self, point, d_point, buttons, modifiers): - if self.panel_opener.is_point_touching(point): - self.panel_opener.match_y(Dot(point)) - self.move_panel_and_controls_to_panel_opener() - return False + def panel_opener_on_mouse_drag(self, mob, event_data): + point = event_data["point"] + self.panel_opener.match_y(Dot(point)) + self.move_panel_and_controls_to_panel_opener() + return False - def on_mouse_scroll(self, point, offset): - if self.panel.is_point_touching(point): - factor = 10 * offset[1] - self.controls.set_y(self.controls.get_y() + factor) - return False + def panel_on_mouse_scroll(self, mob, event_data): + offset = event_data["offset"] + factor = 10 * offset[1] + self.controls.set_y(self.controls.get_y() + factor) + return False diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index fe942dcc..8f711a6d 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -27,6 +27,9 @@ from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import rotation_matrix_transpose from manimlib.shader_wrapper import ShaderWrapper from manimlib.shader_wrapper import get_colormap_code +from manimlib.event_handler import EVENT_DISPATCHER +from manimlib.event_handler.event_listner import EventListner +from manimlib.event_handler.event_type import EventType class Mobject(object): @@ -68,6 +71,7 @@ class Mobject(object): self.init_data() self.init_uniforms() self.init_updaters() + self.init_event_listners() self.init_points() self.init_colors() self.init_shader_data() @@ -1440,35 +1444,81 @@ class Mobject(object): Event handling follows the Event Bubbling model of DOM in javascript. Return false to stop the event bubbling. To learn more visit https://www.quirksmode.org/js/events_order.html + + Event Callback Argument is a callable function taking two arguments: + 1. Mobject + 2. EventData """ - def on_mouse_motion(self, point, d_point): - # To be implemented in subclasses - pass + def init_event_listners(self): + self.event_listners = [] - def on_mouse_drag(self, point, d_point, buttons, modifiers): - # To be implemented in subclasses - pass + def add_event_listner(self, event_type, event_callback): + event_listner = EventListner(self, event_type, event_callback) + self.event_listners.append(event_listner) + EVENT_DISPATCHER.add_listner(event_listner) + return self - def on_mouse_press(self, point, button, mods): - # To be implemented in subclasses - pass + def remove_event_listner(self, event_type, event_callback): + event_listner = EventListner(self, event_type, event_callback) + while event_listner in self.event_listners: + self.event_listners.remove(event_listner) + EVENT_DISPATCHER.remove_listner(event_listner) + return self - def on_mouse_release(self, point, button, mods): - # To be implemented in subclasses - pass + def clear_event_listners(self, recurse=True): + self.event_listners = [] + if recurse: + for submob in self.submobjects: + submob.clear_event_listners(recurse=recurse) + return self - def on_mouse_scroll(self, point, offset): - # To be implemented in subclasses - pass + def get_event_listners(self): + return self.event_listners + + def get_family_event_listners(self): + return list(it.chain(*[sm.get_event_listners() for sm in self.get_family()])) - def on_key_release(self, symbol, modifiers): - # To be implemented in subclasses - pass + def get_has_event_listner(self): + return any( + mob.get_event_listners() + for mob in self.get_family() + ) - def on_key_press(self, symbol, modifiers): - # To be implemented in subclasses - pass + def add_mouse_motion_listner(self, callback): + self.add_event_listner(EventType.MouseMotionEvent, callback) + def remove_mouse_motion_listner(self, callback): + self.remove_event_listner(EventType.MouseMotionEvent, callback) + + def add_mouse_press_listner(self, callback): + self.add_event_listner(EventType.MousePressEvent, callback) + def remove_mouse_press_listner(self, callback): + self.remove_event_listner(EventType.MousePressEvent, callback) + + def add_mouse_release_listner(self, callback): + self.add_event_listner(EventType.MouseReleaseEvent, callback) + def remove_mouse_release_listner(self, callback): + self.remove_event_listner(EventType.MouseReleaseEvent, callback) + + def add_mouse_drag_listner(self, callback): + self.add_event_listner(EventType.MouseDragEvent, callback) + def remove_mouse_drag_listner(self, callback): + self.remove_event_listner(EventType.MouseDragEvent, callback) + + def add_mouse_scroll_listner(self, callback): + self.add_event_listner(EventType.MouseScrollEvent, callback) + def remove_mouse_scroll_listner(self, callback): + self.remove_event_listner(EventType.MouseScrollEvent, callback) + + def add_key_press_listner(self, callback): + self.add_event_listner(EventType.KeyPressEvent, callback) + def remove_key_press_listner(self, callback): + self.remove_event_listner(EventType.KeyPressEvent, callback) + + def add_key_release_listner(self, callback): + self.add_event_listner(EventType.KeyReleaseEvent, callback) + def remove_key_release_listner(self, callback): + self.remove_event_listner(EventType.KeyReleaseEvent, callback) # Errors diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index 1c2b3f19..a61a6eac 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -23,35 +23,6 @@ from manimlib.utils.directories import get_mobject_data_dir from manimlib.utils.images import get_full_vector_image_path -def check_and_fix_percent_bug(sym): - # This is an ugly patch addressing something which should be - # addressed at a deeper level. - # The svg path for percent symbols have a known bug, so this - # checks if the symbol is (probably) a percentage sign, and - # splits it so that it's displayed properly. - if len(sym.get_points()) not in [315, 324, 372, 468, 483] or len(sym.get_subpaths()) != 4: - return - - sym = sym.family_members_with_points()[0] - new_sym = VMobject() - path_lengths = [len(path) for path in sym.get_subpaths()] - sym_points = sym.get_points() - if len(sym_points) in [315, 324, 372]: - n = sum(path_lengths[:2]) - p1 = sym_points[:n] - p2 = sym_points[n:] - elif len(sym_points) in [468, 483]: - p1 = np.vstack([ - sym_points[:path_lengths[0]], - sym_points[-path_lengths[3]:] - ]) - p2 = sym_points[path_lengths[0]:sum(path_lengths[:3])] - sym.set_points(p1) - new_sym.set_points(p2) - sym.add(new_sym) - sym.refresh_triangulation() - - def string_to_numbers(num_string): num_string = num_string.replace("-", ",-") num_string = num_string.replace("e,-", "e-") @@ -379,7 +350,6 @@ class VMobjectFromSVGPathstring(VMobject): self.stretch(-1, 1, about_point=ORIGIN) # Save to a file for future use np.save(points_filepath, self.get_points()) - check_and_fix_percent_bug(self) def get_commands_and_coord_strings(self): all_commands = list(self.get_command_to_function_map().keys()) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 293b7a71..d82a4539 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -19,6 +19,8 @@ from manimlib.scene.scene_file_writer import SceneFileWriter from manimlib.utils.config_ops import digest_config from manimlib.utils.family_ops import extract_mobject_family_members from manimlib.utils.family_ops import restructure_list_to_exclude_certain_family_members +from manimlib.event_handler.event_type import EventType +from manimlib.event_handler import EVENT_DISPATCHER from manimlib.window import Window @@ -513,25 +515,14 @@ class Scene(object): self.mobjects = mobjects # Event handling - def get_event_listeners_mobjects(self): - """ - This method returns all the mobjects that listen to events - in reversed order. So the top most mobject's event is called first. - This helps in event bubbling. - """ - return filter( - lambda mob: mob.listen_to_events, - reversed(self.get_mobject_family_members()) - ) def on_mouse_motion(self, point, d_point): self.mouse_point.move_to(point) - for mob_listener in self.get_event_listeners_mobjects(): - if mob_listener.is_point_touching(point): - propagate_event = mob_listener.on_mouse_motion(point, d_point) - if propagate_event is not None and propagate_event is False: - return + event_data = {"point": point, "d_point": d_point} + propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseMotionEvent, **event_data) + if propagate_event is not None and propagate_event is False: + return frame = self.camera.frame if self.window.is_key_pressed(ord("d")): @@ -548,32 +539,28 @@ class Scene(object): def on_mouse_drag(self, point, d_point, buttons, modifiers): self.mouse_drag_point.move_to(point) - for mob_listener in self.get_event_listeners_mobjects(): - if mob_listener.is_point_touching(point): - propagate_event = mob_listener.on_mouse_drag(point, d_point, buttons, modifiers) - if propagate_event is not None and propagate_event is False: - return + event_data = {"point": point, "d_point": d_point, "buttons": buttons, "modifiers": modifiers} + propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseDragEvent, **event_data) + if propagate_event is not None and propagate_event is False: + return def on_mouse_press(self, point, button, mods): - for mob_listener in self.get_event_listeners_mobjects(): - if mob_listener.is_point_touching(point): - propagate_event = mob_listener.on_mouse_press(point, button, mods) - if propagate_event is not None and propagate_event is False: - return + event_data = {"point": point, "button": button, "mods": mods} + propagate_event = EVENT_DISPATCHER.dispatch(EventType.MousePressEvent, **event_data) + if propagate_event is not None and propagate_event is False: + return def on_mouse_release(self, point, button, mods): - for mob_listener in self.get_event_listeners_mobjects(): - if mob_listener.is_point_touching(point): - propagate_event = mob_listener.on_mouse_release(point, button, mods) - if propagate_event is not None and propagate_event is False: - return + event_data = {"point": point, "button": button, "mods": mods} + propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseReleaseEvent, **event_data) + if propagate_event is not None and propagate_event is False: + return def on_mouse_scroll(self, point, offset): - for mob_listener in self.get_event_listeners_mobjects(): - if mob_listener.is_point_touching(point): - propagate_event = mob_listener.on_mouse_scroll(point, offset) - if propagate_event is not None and propagate_event is False: - return + event_data = {"point": point, "offset": offset} + propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseScrollEvent, **event_data) + if propagate_event is not None and propagate_event is False: + return frame = self.camera.frame if self.window.is_key_pressed(ord("z")): @@ -585,10 +572,10 @@ class Scene(object): frame.shift(-20.0 * shift) def on_key_release(self, symbol, modifiers): - for mob_listener in self.get_event_listeners_mobjects(): - propagate_event = mob_listener.on_key_release(symbol, modifiers) - if propagate_event is not None and propagate_event is False: - return + event_data = {"symbol": symbol, "modifiers": modifiers} + propagate_event = EVENT_DISPATCHER.dispatch(EventType.KeyReleaseEvent, **event_data) + if propagate_event is not None and propagate_event is False: + return def on_key_press(self, symbol, modifiers): try: @@ -597,10 +584,10 @@ class Scene(object): print(" Warning: The value of the pressed key is too large.") return - for mob_listener in self.get_event_listeners_mobjects(): - propagate_event = mob_listener.on_key_press(symbol, modifiers) - if propagate_event is not None and propagate_event is False: - return + event_data = {"symbol": symbol, "modifiers": modifiers} + propagate_event = EVENT_DISPATCHER.dispatch(EventType.KeyPressEvent, **event_data) + if propagate_event is not None and propagate_event is False: + return if char == "r": self.camera.frame.to_default_state() diff --git a/manimlib/shaders/quadratic_bezier_fill/frag.glsl b/manimlib/shaders/quadratic_bezier_fill/frag.glsl index b2a1c82a..c16c167e 100644 --- a/manimlib/shaders/quadratic_bezier_fill/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/frag.glsl @@ -51,7 +51,7 @@ float sdf(){ float sgn = orientation * sign(v2); float Fp = (p.x * p.x - p.y); if(sgn * Fp < 0){ - return 0.0; + return 0; }else{ return min_dist_to_curve(uv_coords, uv_b2, bezier_degree); } diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index f9cfe303..ffb4e8eb 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -66,7 +66,7 @@ void flatten_points(in vec3[3] points, out vec2[3] flat_points){ float angle_between_vectors(vec2 v1, vec2 v2){ float v1_norm = length(v1); float v2_norm = length(v2); - if(v1_norm == 0 || v2_norm == 0) return 0.0; + if(v1_norm == 0 || v2_norm == 0) return 0; float dp = dot(v1, v2) / (v1_norm * v2_norm); float angle = acos(clamp(dp, -1.0, 1.0)); float sn = sign(cross2d(v1, v2)); @@ -269,4 +269,4 @@ void main() { EmitVertex(); } EndPrimitive(); -} +} \ No newline at end of file diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 2a1de082..177f9ae6 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -349,29 +349,41 @@ def norm_squared(v): # TODO, fails for polygons drawn over themselves def earclip_triangulation(verts, rings): + """ + Returns a list of indices giving a triangulation + of a polygon, potentially with holes + + - verts is an NxM numpy array of points with M > 2 + + - rings is a list of indices indicating where + the ends of new paths are + """ n = len(verts) # Establish where loop indices should be connected loop_connections = dict() - for e0, e1 in zip(rings, rings[1:]): - temp_i = e0 - # Find closet point in the first ring (j) to - # the first index of this ring (i) - norms = np.array([ - [j, norm_squared(verts[temp_i] - verts[j])] - for j in range(0, rings[0]) - if j not in loop_connections - ]) - j = int(norms[norms[:, 1].argmin()][0]) - # Find i closest to this j - norms = np.array([ - [i, norm_squared(verts[i] - verts[j])] - for i in range(e0, e1) + # for e0, e1 in zip(rings, rings[1:]): + e0 = rings[0] + for e1 in rings[1:]: + # Find closet pair of points with the first + # coming from the current ring, and the second + # coming from the next ring + index_pairs = [ + (i, j) + for i in range(0, e0) + for j in range(e0, e1) if i not in loop_connections - ]) - i = int(norms[norms[:, 1].argmin()][0]) + if j not in loop_connections + ] + i, j = index_pairs[np.argmin([ + norm_squared(verts[i] - verts[j]) + for i, j in index_pairs + ])] + # Connect the polygon at these points so that + # it's treated as a single highly-convex ring loop_connections[i] = j loop_connections[j] = i + e0 = e1 # Setup linked list after = [] @@ -397,87 +409,3 @@ def earclip_triangulation(verts, rings): meta_indices = earcut(verts[indices, :2], [len(indices)]) return [indices[mi] for mi in meta_indices] - - -def old_earclip_triangulation(verts, rings, orientation): - n = len(verts) - assert(n in rings) - result = [] - - # Establish where loop indices should be connected - loop_connections = dict() - e0 = 0 - for e1 in rings: - norms = np.array([ - [i, j, get_norm(verts[i] - verts[j])] - for i in range(e0, e1) - for j in it.chain(range(0, e0), range(e1, n)) - ]) - if len(norms) == 0: - continue - i, j = norms[np.argmin(norms[:, 2])][:2].astype(int) - loop_connections[i] = j - loop_connections[j] = i - e0 = e1 - - # Setup bidirectional linked list - before = [] - after = [] - e0 = 0 - for e1 in rings: - after += [*range(e0 + 1, e1), e0] - before += [e1 - 1, *range(e0, e1 - 1)] - e0 = e1 - - # Initialize edge triangles - edge_tris = [] - i = 0 - starting = True - while (i != 0 or starting): - starting = False - if i in loop_connections: - j = loop_connections[i] - edge_tris.append([before[i], i, j]) - edge_tris.append([i, j, after[j]]) - i = after[j] - else: - edge_tris.append([before[i], i, after[i]]) - i = after[i] - - # Set up a test for whether or not three indices - # form an ear of the polygon, meaning a convex corner - # which doesn't contain any other vertices - indices = list(range(n)) - - def is_ear(*tri_indices): - tri = [verts[i] for i in tri_indices] - v1 = tri[1] - tri[0] - v2 = tri[2] - tri[1] - cross = v1[0] * v2[1] - v2[0] * v1[1] - if orientation * cross < 0: - return False - for j in indices: - if j in tri_indices: - continue - elif is_inside_triangle(verts[j], *tri): - return False - return True - - # Loop through and clip off all the ears - n_failures = 0 - i = 0 - while n_failures < len(edge_tris): - n = len(edge_tris) - edge_tri = edge_tris[i % n] - if is_ear(*edge_tri): - result.extend(edge_tri) - edge_tris[(i - 1) % n][2] = edge_tri[2] - edge_tris[(i + 1) % n][0] = edge_tri[0] - if edge_tri[1] in indices: - indices.remove(edge_tri[1]) - edge_tris.remove(edge_tri) - n_failures = 0 - else: - n_failures += 1 - i += 1 - return result