From f3d2c4e7310378e5d7e21c5fab0597d280dba5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Eertmans?= Date: Tue, 12 Jul 2022 17:11:32 +0200 Subject: [PATCH] Refactor old work --- manim_presentation/__init__.py | 1 - manim_presentation/wizard.py | 51 ---------- manim_slides/__init__.py | 3 + manim_slides/commons.py | 27 ++++++ manim_slides/config.py | 48 ++++++++++ manim_slides/defaults.py | 11 +++ manim_slides/main.py | 20 ++++ .../present.py | 96 ++++++++----------- {manim_presentation => manim_slides}/slide.py | 18 ++-- manim_slides/wizard.py | 71 ++++++++++++++ 10 files changed, 232 insertions(+), 114 deletions(-) delete mode 100644 manim_presentation/__init__.py delete mode 100644 manim_presentation/wizard.py create mode 100644 manim_slides/__init__.py create mode 100644 manim_slides/commons.py create mode 100644 manim_slides/config.py create mode 100644 manim_slides/defaults.py create mode 100644 manim_slides/main.py rename {manim_presentation => manim_slides}/present.py (78%) rename {manim_presentation => manim_slides}/slide.py (89%) create mode 100644 manim_slides/wizard.py diff --git a/manim_presentation/__init__.py b/manim_presentation/__init__.py deleted file mode 100644 index 4c18684..0000000 --- a/manim_presentation/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from manim_presentation.slide import Slide \ No newline at end of file diff --git a/manim_presentation/wizard.py b/manim_presentation/wizard.py deleted file mode 100644 index d4733d5..0000000 --- a/manim_presentation/wizard.py +++ /dev/null @@ -1,51 +0,0 @@ -import cv2 -import numpy as np -import json -import os -import sys - -def prompt(question): - font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255) - display = np.zeros((130, 420), np.uint8) - - cv2.putText( - display, - "* Manim Presentation Wizard *", - (50, 33), - *font_args - ) - cv2.putText( - display, - question, - (30, 85), - *font_args - ) - - cv2.imshow("wizard", display) - return cv2.waitKeyEx(-1) - -def main(): - if(os.path.exists("./manim-presentation.json")): - print("The manim-presentation.json configuration file exists") - ans = input("Do you want to continue and overwrite it? (y/n): ") - if ans != "y": sys.exit(0) - - prompt("Press any key to continue") - PLAYPAUSE_KEY = prompt("Press the PLAY/PAUSE key") - CONTINUE_KEY = prompt("Press the CONTINUE/NEXT key") - BACK_KEY = prompt("Press the BACK key") - REWIND_KEY = prompt("Press the REWIND key") - QUIT_KEY = prompt("Press the QUIT key") - - config_file = open("./manim-presentation.json", "w") - json.dump(dict( - PLAYPAUSE_KEY=PLAYPAUSE_KEY, - CONTINUE_KEY=CONTINUE_KEY, - BACK_KEY=BACK_KEY, - REWIND_KEY=REWIND_KEY, - QUIT_KEY=QUIT_KEY - ), config_file) - config_file.close() - - - \ No newline at end of file diff --git a/manim_slides/__init__.py b/manim_slides/__init__.py new file mode 100644 index 0000000..e2165b1 --- /dev/null +++ b/manim_slides/__init__.py @@ -0,0 +1,3 @@ +from .slide import Slide, ThreeDSlide + +__version__ = "3.0.0" diff --git a/manim_slides/commons.py b/manim_slides/commons.py new file mode 100644 index 0000000..e753f4f --- /dev/null +++ b/manim_slides/commons.py @@ -0,0 +1,27 @@ +import click +from .defaults import CONFIG_PATH + + +def config_path_option(function): + return click.option( + "-c", + "--config", + "config_path", + default=CONFIG_PATH, + type=click.Path(dir_okay=False), + help="Set path to configuration file.", + )(function) + + +def config_options(function): + function = config_path_option(function) + function = click.option( + "-f", "--force", is_flag=True, help="Overwrite any existing configuration file." + )(function) + function = click.option( + "-m", + "--merge", + is_flag=True, + help="Merge any existing configuration file with the new configuration.", + )(function) + return function diff --git a/manim_slides/config.py b/manim_slides/config.py new file mode 100644 index 0000000..7f777e0 --- /dev/null +++ b/manim_slides/config.py @@ -0,0 +1,48 @@ +from typing import Optional, Set + +from pydantic import BaseModel, root_validator, validator + +from .defaults import LEFT_ARROW_KEY_CODE, RIGHT_ARROW_KEY_CODE + + +class Key(BaseModel): + ids: Set[int] + name: Optional[str] = None + + @validator("ids", each_item=True) + def id_is_posint(cls, v: int): + if v < 0: + raise ValueError("Key ids cannot be negative integers") + return v + + def match(self, key_id: int): + return key_id in self.ids + + +class Config(BaseModel): + QUIT: Key = Key(ids=[ord("q")], name="QUIT") + CONTINUE: Key = Key(ids=[RIGHT_ARROW_KEY_CODE], name="CONTINUE / NEXT") + BACK: Key = Key(ids=[LEFT_ARROW_KEY_CODE], name="BACK") + REWIND: Key = Key(ids=[ord("r")], name="REWIND") + PLAY_PAUSE: Key = Key(ids=[32], name="PLAY / PAUSE") + + @root_validator + def ids_are_unique_across_keys(cls, values): + ids = set() + + for key in values.values(): + if len(ids.intersection(key.ids)) != 0: + raise ValueError( + f"Two or more keys share a common key code: please make sure each key has distinc key codes" + ) + ids.update(key.ids) + + return values + + def merge_with(self, other: "Config") -> "Config": + for key_name, key in self: + other_key = getattr(other, key_name) + key.ids.update(other_key.ids) + key.name = other_key.name or key.name + + return self diff --git a/manim_slides/defaults.py b/manim_slides/defaults.py new file mode 100644 index 0000000..f63a375 --- /dev/null +++ b/manim_slides/defaults.py @@ -0,0 +1,11 @@ +import platform + +FOLDER_PATH: str = "./slides" +CONFIG_PATH: str = ".manim-slides.json" + +if platform.system() == "Windows": + RIGHT_ARROW_KEY_CODE = 2555904 + LEFT_ARROW_KEY_CODE = 2424832 +else: + RIGHT_ARROW_KEY_CODE = 65363 + LEFT_ARROW_KEY_CODE = 65361 diff --git a/manim_slides/main.py b/manim_slides/main.py new file mode 100644 index 0000000..7cf24e4 --- /dev/null +++ b/manim_slides/main.py @@ -0,0 +1,20 @@ +import click +from click_default_group import DefaultGroup + +from . import __version__ +from .present import present +from .wizard import wizard, init + + +@click.group(cls=DefaultGroup, default="present", default_if_no_args=True) +@click.version_option(__version__, "-v", "--version") +@click.help_option("-h", "--help") +def cli(): + pass + +cli.add_command(present) +cli.add_command(wizard) +cli.add_command(init) + +if __name__ == "__main__": + cli() diff --git a/manim_presentation/present.py b/manim_slides/present.py similarity index 78% rename from manim_presentation/present.py rename to manim_slides/present.py index 87ea02c..305e042 100644 --- a/manim_presentation/present.py +++ b/manim_slides/present.py @@ -1,34 +1,18 @@ -import cv2 -import numpy as np -import os -import sys import json import math +import os +import sys import time -import argparse from enum import Enum -import platform -class Config: - @classmethod - def init(cls): - if platform.system() == "Windows": - cls.QUIT_KEY = ord("q") - cls.CONTINUE_KEY = 2555904 #right arrow - cls.BACK_KEY = 2424832 #left arrow - cls.REWIND_KEY = ord("r") - cls.PLAYPAUSE_KEY = 32 #spacebar - else: - cls.QUIT_KEY = ord("q") - cls.CONTINUE_KEY = 65363 #right arrow - cls.BACK_KEY = 65361 #left arrow - cls.REWIND_KEY = ord("r") - cls.PLAYPAUSE_KEY = 32 #spacebar - - if os.path.exists(os.path.join(os.getcwd(), "./manim-presentation.json")): - json_config = json.load(open(os.path.join(os.getcwd(), "./manim-presentation.json"), "r")) - for key, value in json_config.items(): - setattr(cls, key, value) +import click +import cv2 +import numpy as np + +from .config import Config +from .defaults import CONFIG_PATH, FOLDER_PATH +from .commons import config_path_option + class State(Enum): PLAYING = 0 @@ -50,7 +34,7 @@ def fix_time(x): return x if x > 0 else 1 class Presentation: - def __init__(self, config, last_frame_next=False): + def __init__(self, config, last_frame_next: bool = False): self.last_frame_next = last_frame_next self.slides = config["slides"] self.files = config["files"] @@ -164,9 +148,10 @@ class Presentation: class Display: - def __init__(self, presentations, start_paused=False, fullscreen=False): + def __init__(self, presentations, config, start_paused=False, fullscreen=False): self.presentations = presentations self.start_paused = start_paused + self.config = config self.state = State.PLAYING self.lastframe = None @@ -243,25 +228,25 @@ class Display: ((grid_x[0]+grid_x[1])//2, grid_y[2]), *font_args ) - + cv2.imshow("Info", info) def handle_key(self): sleep_time = math.ceil(1000/self.current_presentation.fps) key = cv2.waitKeyEx(fix_time(sleep_time - self.lag)) - if key == Config.QUIT_KEY: + if self.config.QUIT.match(key): self.quit() - elif self.state == State.PLAYING and key == Config.PLAYPAUSE_KEY: + elif self.state == State.PLAYING and self.config.PLAY_PAUSE.match(key): self.state = State.PAUSED - elif self.state == State.PAUSED and key == Config.PLAYPAUSE_KEY: + elif self.state == State.PAUSED and self.config.PLAY_PAUSE.match(key): self.state = State.PLAYING - elif self.state == State.WAIT and (key == Config.CONTINUE_KEY or key == Config.PLAYPAUSE_KEY): + elif self.state == State.WAIT and (self.config.CONTINUE.match(key) or self.config.PLAY_PAUSE.match(key)): self.current_presentation.next() self.state = State.PLAYING - elif self.state == State.PLAYING and key == Config.CONTINUE_KEY: + elif self.state == State.PLAYING and self.config.CONTINUE.match(key): self.current_presentation.next() - elif key == Config.BACK_KEY: + elif self.config.BACK.match(key): if self.current_presentation.current_slide_i == 0: self.current_presentation_i = max(0, self.current_presentation_i - 1) self.current_presentation.reset() @@ -269,7 +254,7 @@ class Display: else: self.current_presentation.prev() self.state = State.PLAYING - elif key == Config.REWIND_KEY: + elif self.config.REWIND.match(key): self.current_presentation.rewind_slide() self.state = State.PLAYING @@ -279,30 +264,29 @@ class Display: sys.exit() -def main(): - parser = argparse.ArgumentParser() - - parser.add_argument("scenes", metavar="scenes", type=str, nargs="+", help="Scenes to present") - parser.add_argument("--folder", type=str, default="./presentation", help="Presentation files folder") - parser.add_argument("--start-paused", action="store_true", help="Start paused") - parser.add_argument("--fullscreen", action="store_true", help="Fullscreen") - parser.add_argument("--last-frame-next", action="store_true", help="Show the next animation first frame as last frame (hack)") - - args = parser.parse_args() - args.folder = os.path.normcase(args.folder) - - Config.init() +@click.command() +@click.argument("scenes", nargs=-1) +@config_path_option +@click.option("--folder", default=FOLDER_PATH, type=click.Path(exists=True, file_okay=False), help="Set slides folder.") +@click.option("--start-paused", is_flag=True, help="Start paused.") +@click.option("--fullscreen", is_flag=True, help="Fullscreen mode.") +@click.option("--last-frame-next", is_flag=True, help="Show the next animation first frame as last frame (hack).") +@click.help_option("-h", "--help") +def present(scenes, config_path, folder, start_paused, fullscreen, last_frame_next): + """Present the different scenes""" presentations = list() - for scene in args.scenes: - config_file = os.path.join(args.folder, f"{scene}.json") + for scene in scenes: + config_file = os.path.join(folder, f"{scene}.json") if not os.path.exists(config_file): raise Exception(f"File {config_file} does not exist, check the scene name and make sure to use Slide as your scene base class") config = json.load(open(config_file)) - presentations.append(Presentation(config, last_frame_next=args.last_frame_next)) + presentations.append(Presentation(config, last_frame_next=last_frame_next)) - display = Display(presentations, start_paused=args.start_paused, fullscreen=args.fullscreen) + if os.path.exists(config_path): + config = Config.parse_file(config_path) + else: + config = Config() + + display = Display(presentations, config=config, start_paused=start_paused, fullscreen=fullscreen) display.run() - -if __name__ == "__main__": - main() diff --git a/manim_presentation/slide.py b/manim_slides/slide.py similarity index 89% rename from manim_presentation/slide.py rename to manim_slides/slide.py index b385110..03ff9e7 100644 --- a/manim_presentation/slide.py +++ b/manim_slides/slide.py @@ -1,13 +1,16 @@ -import os import json +import os import shutil -from manim import Scene, config -from manim.animation.animation import Wait + +from manim import Scene, ThreeDScene, config + +from .defaults import FOLDER_PATH + class Slide(Scene): - def __init__(self, *args, **kwargs): - self.output_folder = kwargs.pop("output_folder", "./presentation") + def __init__(self, *args, output_folder=FOLDER_PATH, **kwargs): super(Slide, self).__init__(*args, **kwargs) + self.output_folder = output_folder self.slides = list() self.current_slide = 1 self.current_animation = 0 @@ -29,7 +32,7 @@ class Slide(Scene): self.pause_start_animation = self.current_animation def start_loop(self): - assert self.loop_start_animation is None, "You cant nest loops" + assert self.loop_start_animation is None, "You cannot nest loops" self.loop_start_animation = self.current_animation def end_loop(self): @@ -81,3 +84,6 @@ class Slide(Scene): files=files ), f) f.close() + +class ThreeDSlide(ThreeDScene, Slide): + pass diff --git a/manim_slides/wizard.py b/manim_slides/wizard.py new file mode 100644 index 0000000..dca1a64 --- /dev/null +++ b/manim_slides/wizard.py @@ -0,0 +1,71 @@ +import os +import sys + +import click +import cv2 +import numpy as np + +from .config import Config +from .commons import config_options +from .defaults import CONFIG_PATH + + +def prompt(question: str) -> int: + font_args = (cv2.FONT_HERSHEY_SIMPLEX, 0.7, 255) + display = np.zeros((130, 420), np.uint8) + + cv2.putText(display, "* Manim Slides Wizard *", (70, 33), *font_args) + cv2.putText(display, question, (30, 85), *font_args) + + cv2.imshow("Manim Slides Configuration Wizard", display) + return cv2.waitKeyEx(-1) + + +@click.command() +@config_options +def wizard(config_path, force, merge): + """Launch configuration wizard.""" + return _init(config_path, force, merge, skip_interactive=False) + +@click.command() +@config_options +def init(config_path, force, merge, skip_interactive=False): + """Initialize a new default configuration file.""" + return _init(config_path, force, merge, skip_interactive=True) + +def _init(config_path, force, merge, skip_interactive=False): + + if os.path.exists(config_path): + click.secho(f"The `{CONFIG_PATH}` configuration file exists") + + if not force and not merge: + choice = click.prompt("Do you want to continue and (o)verwrite / (m)erge it, or (q)uit?", type=click.Choice(["o", "m", "q"], case_sensitive=False)) + + force = choice == "o" + merge = choice == "m" + + if force: + click.secho("Overwriting.") + elif merge: + click.secho("Merging.") + else: + click.secho("Exiting.") + sys.exit(0) + + + config = Config() + + if not skip_interactive: + + prompt("Press any key to continue") + + for _, key in config: + key.ids = [prompt(f"Press the {key.name} key")] + + if merge: + config = Config.parse_file(config_path).merge_with(config) + + with open(config_path, "w") as config_file: + config_file.write(config.json(indent=4)) + + click.echo(f"Configuration file successfully save to `{config_path}`")