From 47d7c8d6c58a7a6459c12f2adc2bc7c3c7e72dac Mon Sep 17 00:00:00 2001 From: shavez Date: Fri, 18 Jul 2025 16:46:37 +0530 Subject: [PATCH] feat: integrate ImGui window into interactive embed loop for real-time controls --- manimlib/camera/camera.py | 8 ++++++++ manimlib/scene/scene.py | 25 ++++++++++++++++++++----- manimlib/window.py | 20 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index a423465dda..247a4223e7 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -20,12 +20,14 @@ from typing import Optional from manimlib.typing import ManimColor, Vect3 from manimlib.window import Window + from manimlib.scene.scene import Scene class Camera(object): def __init__( self, window: Optional[Window] = None, + scene: Optional[Scene] = None, background_image: Optional[str] = None, frame_config: dict = dict(), # Note: frame height and width will be resized to match this resolution aspect ratio @@ -44,8 +46,10 @@ def __init__( # without multisampling, for 3d scenes one might want # to set samples to be greater than 0. samples: int = 0, + imgui: bool = True, ): self.window = window + self.scene = scene self.background_image = background_image self.default_pixel_shape = resolution # Rename? self.fps = fps @@ -55,6 +59,7 @@ def __init__( self.pixel_array_dtype = pixel_array_dtype self.light_source_position = light_source_position self.samples = samples + self.imgui = imgui self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max self.background_rgba: list[float] = list(color_to_rgba( @@ -230,6 +235,9 @@ def capture(self, *mobjects: Mobject) -> None: mobject.render(self.ctx, self.uniforms) if self.window: + if self.imgui and self.scene is not None: + self.scene.construct_imgui_window() + self.scene.render_imgui() self.window.swap_buffers() if self.fbo is not self.window_fbo: self.blit(self.fbo, self.window_fbo) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 6ca06c3faa..a5901f3b5e 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -8,6 +8,8 @@ from contextlib import contextmanager from contextlib import ExitStack +import imgui + import numpy as np from tqdm.auto import tqdm as ProgressDisplay from pyglet.window import key as PygletWindowKeys @@ -60,6 +62,7 @@ class Scene(object): samples = 0 # Euler angles, in degrees default_frame_orientation = (0, 0) + imgui: bool = False def __init__( self, @@ -102,10 +105,12 @@ def __init__( self.window.init_for_scene(self) # Make sure camera and Pyglet window sync self.camera_config["fps"] = 30 + self.camera_config["imgui"] = self.imgui # Core state of the scene self.camera: Camera = Camera( window=self.window, + scene=self, samples=self.samples, **self.camera_config ) @@ -154,6 +159,7 @@ def run(self) -> None: self.setup() try: self.construct() + self.construct_imgui_window() self.interact() except EndScene: pass @@ -176,6 +182,9 @@ def construct(self) -> None: # To be implemented in subclasses pass + def construct_imgui_window(self): + pass + def tear_down(self) -> None: self.stop_skipping() self.file_writer.finish() @@ -220,6 +229,10 @@ def embed( # Only these methods should touch the camera + def render_imgui(self): + imgui.render() + self.window.impl.render(imgui.get_draw_data()) + def get_image(self) -> Image: if self.window is not None: self.camera.use_window_fbo(False) @@ -766,7 +779,8 @@ def on_mouse_drag( ) -> None: self.mouse_drag_point.move_to(point) if self.drag_to_pan: - self.frame.shift(-d_point) + if not self.window.io.want_capture_mouse: + self.frame.shift(-d_point) event_data = {"point": point, "d_point": d_point, "buttons": buttons, "modifiers": modifiers} propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseDragEvent, **event_data) @@ -809,10 +823,11 @@ def on_mouse_scroll( return rel_offset = y_pixel_offset / self.camera.get_pixel_height() - self.frame.scale( - 1 - self.scroll_sensitivity * rel_offset, - about_point=point - ) + if not self.window.io.want_capture_mouse: + self.frame.scale( + 1 - self.scroll_sensitivity * rel_offset, + about_point=point + ) def on_key_release( self, diff --git a/manimlib/window.py b/manimlib/window.py index ad2a92d3ee..90d18f331b 100644 --- a/manimlib/window.py +++ b/manimlib/window.py @@ -2,6 +2,10 @@ import numpy as np +import imgui +from moderngl_window.integrations.imgui import ModernglWindowRenderer +from moderngl_window.context.pyglet.keys import Keys as PygletKeys + import moderngl_window as mglw from moderngl_window.context.pyglet.window import Window as PygletWindow from moderngl_window.timers.clock import Timer @@ -48,6 +52,10 @@ def __init__( if self.scene: self.init_for_scene(scene) + + imgui.create_context() + self.impl = ModernglWindowRenderer(self) + self.io = imgui.get_io() def init_for_scene(self, scene: Scene): """ @@ -150,6 +158,9 @@ def wrapper(self, *args, **kwargs): func(self, *args, **kwargs) self._has_undrawn_event = True return wrapper + + def imgui_y(self, y): + return self.height - y @note_undrawn_event def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None: @@ -159,6 +170,7 @@ def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None: point = self.pixel_coords_to_space_coords(x, y) d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True) self.scene.on_mouse_motion(point, d_point) + self.impl.mouse_position_event(x, self.imgui_y(y), dx, dy) @note_undrawn_event def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None: @@ -168,6 +180,7 @@ def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifier point = self.pixel_coords_to_space_coords(x, y) d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True) self.scene.on_mouse_drag(point, d_point, buttons, modifiers) + self.impl.mouse_drag_event(x, self.imgui_y(y), dx, dy) @note_undrawn_event def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None: @@ -176,6 +189,7 @@ def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None: return point = self.pixel_coords_to_space_coords(x, y) self.scene.on_mouse_press(point, button, mods) + self.impl.mouse_press_event(x, self.imgui_y(y), button) @note_undrawn_event def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None: @@ -184,6 +198,7 @@ def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None: return point = self.pixel_coords_to_space_coords(x, y) self.scene.on_mouse_release(point, button, mods) + self.impl.mouse_release_event(x, self.imgui_y(y), button) @note_undrawn_event def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None: @@ -193,6 +208,7 @@ def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> N point = self.pixel_coords_to_space_coords(x, y) offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True) self.scene.on_mouse_scroll(point, offset, x_offset, y_offset) + self.impl.mouse_scroll_event(x_offset, y_offset) @note_undrawn_event def on_key_press(self, symbol: int, modifiers: int) -> None: @@ -201,6 +217,8 @@ def on_key_press(self, symbol: int, modifiers: int) -> None: if not self.scene: return self.scene.on_key_press(symbol, modifiers) + self.impl.key_event(symbol, PygletKeys.ACTION_PRESS, modifiers) + self.io.add_input_character(symbol) @note_undrawn_event def on_key_release(self, symbol: int, modifiers: int) -> None: @@ -209,6 +227,7 @@ def on_key_release(self, symbol: int, modifiers: int) -> None: if not self.scene: return self.scene.on_key_release(symbol, modifiers) + self.impl.key_event(symbol, PygletKeys.ACTION_RELEASE, modifiers) @note_undrawn_event def on_resize(self, width: int, height: int) -> None: @@ -216,6 +235,7 @@ def on_resize(self, width: int, height: int) -> None: if not self.scene: return self.scene.on_resize(width, height) + self.impl.resize(width, height) @note_undrawn_event def on_show(self) -> None: