mirror of
https://github.com/3b1b/manim.git
synced 2025-09-01 00:48:45 +00:00
Merge pull request #1 from 3b1b/master
This commit is contained in:
commit
37f1130de6
17 changed files with 376 additions and 273 deletions
14
.github/ISSUE_TEMPLATE.md
vendored
14
.github/ISSUE_TEMPLATE.md
vendored
|
@ -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.
|
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Code**:
|
||||
<!-- The code you run which reflect the bug. -->
|
||||
|
||||
**Wrong display or Error traceback**:
|
||||
<!-- the wrong display result of the code you run, or the error Traceback -->
|
||||
|
||||
### Additional context
|
||||
<!-- Add any other context about the problem here. -->
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -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.
|
23
.github/ISSUE_TEMPLATE/error-when-using.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/error-when-using.md
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: Error when using
|
||||
about: The error you encountered while using manim
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Describe the error
|
||||
<!-- A clear and concise description of what you want to make. -->
|
||||
|
||||
### Code and Error
|
||||
**Code**:
|
||||
<!-- The code you run -->
|
||||
|
||||
**Error**:
|
||||
<!-- The error traceback you get when run your code -->
|
||||
|
||||
### Environment
|
||||
**OS System**:
|
||||
**manim version**: master <!-- make sure you are using the latest version of master branch -->
|
||||
**python version**:
|
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,9 +1,18 @@
|
|||
Thanks for contributing to manim!
|
||||
<!-- Thanks for contributing to manim!
|
||||
Please ensure that your pull request works with the latest version of manim.
|
||||
-->
|
||||
|
||||
**Please ensure that your pull request works with the latest version of manim.**
|
||||
You should also include:
|
||||
## Motivation
|
||||
<!-- Outline your motivation: In what way do your changes improve the library? -->
|
||||
|
||||
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
|
||||
<!-- What you changed in those files -->
|
||||
-
|
||||
-
|
||||
-
|
||||
|
||||
## Test
|
||||
<!-- How do you test your changes -->
|
||||
**Code**:
|
||||
|
||||
**Result**:
|
|
@ -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
|
||||
|
|
6
manimlib/event_handler/__init__.py
Normal file
6
manimlib/event_handler/__init__.py
Normal file
|
@ -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()
|
92
manimlib/event_handler/event_dispatcher.py
Normal file
92
manimlib/event_handler/event_dispatcher.py
Normal file
|
@ -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
|
15
manimlib/event_handler/event_listner.py
Normal file
15
manimlib/event_handler/event_listner.py
Normal file
|
@ -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
|
11
manimlib/event_handler/event_type.py
Normal file
11
manimlib/event_handler/event_type.py
Normal file
|
@ -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'
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue