mirror of
https://github.com/3b1b/videos.git
synced 2025-09-18 21:38:53 +00:00
Add references to my sublime workflow
This commit is contained in:
parent
2c1a15a3e5
commit
9840f3bd7b
8 changed files with 306 additions and 2 deletions
43
README.md
43
README.md
|
@ -1,7 +1,46 @@
|
|||
|
||||
This project contains the code used to generate the explanatory math videos found on [3Blue1Brown](https://www.3blue1brown.com/).
|
||||
|
||||
This almost entirely consists of scenes generated using the library [Manim](https://github.com/3b1b/manim). See also the community maintained version at [ManimCommunity](https://github.com/ManimCommunity/manim/).
|
||||
|
||||
Note, while the library Manim itself is open source and under the MIT license, the contents of this project are intended only to be used for 3Blue1Brown videos themselves.
|
||||
Older projects may have code dependent on older versions of manim, and so may not run out of the box here.
|
||||
|
||||
Copyright © 2022 3Blue1Brown
|
||||
Note, while the library Manim itself is open source and under the MIT license, the contents of this repository are intended only to be used for 3Blue1Brown videos themselves.
|
||||
|
||||
## Workflow
|
||||
|
||||
I made this video to show more of how I use manim. A lot of my workflow depends on some custom plugins with Sublime, which is the text editor I use, and below I've outlined what's involved for those who want to try it out themselves. I also use macOS, and it's very possible some of what I've written won't work on other operating systems.
|
||||
|
||||
If you use another text editor, the same functionality can, I'm sure, be mimicked. The key is to make use of two facts.
|
||||
|
||||
- Running manim with the arguments "-se (line_number)" will drop you into an interactive mode at that line, like a debugger, with an iPython terminal that can be used to interact with the scene.
|
||||
|
||||
- Within that interactive mode, if you enter "checkpoint_paste()" to the terminal, it will run whatever bit of code is copied to the clipboard. Moreover, if that copied code begins with a comment, the first time it sees that comment it will save the state of the scene at that point, and for all future calls on code beginning with the same comment, it will first revert to that state of the scene before running the code.
|
||||
- The argument "skip" of checkpoint_paste will mean it runs the code without animating, as if all run times set to 0.
|
||||
- The argument "record" of checkpoint_paste will cause whatever animations are run with that copied code to be rendered to file.
|
||||
|
||||
|
||||
### Sublime-specific instructions
|
||||
|
||||
Install [Terminus](https://packagecontrol.io/packages/Terminus) (via package control). This is a terminal run within sublime, and it lets us write some plugins that take the state in sublime, like where your cursor is, what's highlighted, etc., and use that to run a desired command line instruction.
|
||||
|
||||
Take the files in the "sublime_custom_commands" sub-directory of this repo, and copy them into the Packages/User/ directory of your Sublime Application. This should be a directory with a path that looks something like /wherever/your/sublime/lives/Packages/User/
|
||||
|
||||
Add some keybindings to reference these commands. Here's what I have inside my key_bindings file, you can find your own under the menu Sublime Text -> Settings -> Keybindings
|
||||
|
||||
```
|
||||
{ "keys": ["shift+super+r"], "command": "manim_run_scene" },
|
||||
{ "keys": ["super+r"], "command": "manim_checkpoint_paste" },
|
||||
{ "keys": ["super+alt+r"], "command": "manim_recorded_checkpoint_paste" },
|
||||
{ "keys": ["super+ctrl+r"], "command": "manim_skipped_checkpoint_paste" },
|
||||
{ "keys": ["super+e"], "command": "manim_exit" },
|
||||
{ "keys": ["super+option+/"], "command": "comment_fold"},
|
||||
```
|
||||
|
||||
For example, I bind the "command + shift + R" to a custom "manim_run_scene" command. If the cursor is inside a line of a scene, this will drop you into the interactive mode at that point of the scene. If the cursor is on the line defining the scene, it will copy to the clipboard the command needed to render that full scene to file.
|
||||
|
||||
I bind "command + R" to a "manim_checkpoint_paste" command, which will copy whatever bit of code is highlighted, and run "checkpoint_paste()" in the interactive terminal.
|
||||
|
||||
Of course, you could set these to whatever keyboard shortcuts you prefer.
|
||||
|
||||
Copyright © 2024 3Blue1Brown
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"caption": "ManimCheckpointPaste: Copy selection, run `checkpoint_paste` in terminal",
|
||||
"command": "manim_checkpoint_paste"
|
||||
}
|
||||
]
|
6
sublime_custom_commands/ManimExit.sublime-commands
Normal file
6
sublime_custom_commands/ManimExit.sublime-commands
Normal file
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"caption": "ManimExit: Exit manim scene",
|
||||
"command": "manim_exit"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"caption": "ManimRecordedCheckpointPaste: Copy selection, run `checkpoint_paste(record=True)` in terminal",
|
||||
"command": "manim_recorded_checkpoint_paste"
|
||||
}
|
||||
]
|
6
sublime_custom_commands/ManimRunScene.sublime-commands
Normal file
6
sublime_custom_commands/ManimRunScene.sublime-commands
Normal file
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"caption": "ManimRunScene: Run highlighted manim scene",
|
||||
"command": "manim_run_scene"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"caption": "ManimSkippedCheckpointPaste: Copy selection, run `checkpoint_paste(skip=True)` in terminal",
|
||||
"command": "manim_skipped_checkpoint_paste"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"caption": "OpenMirroredDirectory: Open mirrored directory in finder",
|
||||
"command": "open_mirrored_directory"
|
||||
}
|
||||
]
|
229
sublime_custom_commands/manim_plugins.py
Normal file
229
sublime_custom_commands/manim_plugins.py
Normal file
|
@ -0,0 +1,229 @@
|
|||
import sublime_plugin
|
||||
import sublime
|
||||
import os
|
||||
import subprocess as sp
|
||||
import threading
|
||||
import re
|
||||
import time
|
||||
|
||||
|
||||
def get_command(view, window):
|
||||
file_path = os.path.join(
|
||||
window.extract_variables()["file_path"],
|
||||
window.extract_variables()["file_name"],
|
||||
)
|
||||
|
||||
# Pull out lines of file
|
||||
contents = view.substr(sublime.Region(0, view.size()))
|
||||
all_lines = contents.split("\n")
|
||||
|
||||
# Find which lines define classes
|
||||
class_lines = [
|
||||
(line, all_lines.index(line))
|
||||
for line in contents.split("\n")
|
||||
if re.match(r"class (.+?)\((.+?)\):", line)
|
||||
]
|
||||
|
||||
# Where is the cursor
|
||||
row, col = view.rowcol(view.sel()[0].begin())
|
||||
|
||||
# Find the first class defined before where the cursor is
|
||||
try:
|
||||
matching_class_str, scene_line_no = next(filter(
|
||||
lambda cl: cl[1] <= row,
|
||||
reversed(class_lines)
|
||||
))
|
||||
except StopIteration:
|
||||
raise Exception("No matching classes")
|
||||
scene_name = matching_class_str[len("class "):matching_class_str.index("(")]
|
||||
|
||||
cmds = ["manimgl", file_path, scene_name]
|
||||
enter = False
|
||||
|
||||
if row != scene_line_no:
|
||||
cmds.append(f"-se {row + 1}")
|
||||
enter = True
|
||||
|
||||
return " ".join(cmds), enter
|
||||
|
||||
|
||||
def send_terminus_command(
|
||||
command,
|
||||
clear=True,
|
||||
center=True,
|
||||
enter=True,
|
||||
):
|
||||
# Find terminus window
|
||||
terminal_sheet = find_terminus_sheet()
|
||||
if terminal_sheet is None:
|
||||
return
|
||||
window = terminal_sheet.window()
|
||||
view = terminal_sheet.view()
|
||||
_, col = view.rowcol(view.size())
|
||||
|
||||
# Ammend command with various keyboard shortcuts
|
||||
full_command = "".join([
|
||||
"\x7F" * col if clear else "", # Bad hack
|
||||
"\x0C" if center else "", # Command + l
|
||||
command,
|
||||
"\n" if enter else "",
|
||||
])
|
||||
window.run_command("terminus_send_string", {"string": full_command})
|
||||
|
||||
|
||||
def find_terminus_sheet():
|
||||
for win in sublime.windows():
|
||||
for sheet in win.sheets():
|
||||
name = sheet.view().name()
|
||||
if name == "Login Shell" or name.startswith("IPython: "):
|
||||
return sheet
|
||||
return None
|
||||
|
||||
|
||||
def ensure_terminus_tab_exists():
|
||||
"""
|
||||
If there is no sheet with a terminus tab,
|
||||
it opens a new window with one.
|
||||
Returns a timeout period suitable for
|
||||
following commands
|
||||
"""
|
||||
if find_terminus_sheet() is None:
|
||||
sublime.run_command('new_window')
|
||||
new_window = next(reversed(sublime.windows()))
|
||||
new_window.run_command("terminus_open")
|
||||
return 500
|
||||
return 0
|
||||
|
||||
|
||||
def checkpoint_paste_wrapper(view, arg_str=""):
|
||||
window = view.window()
|
||||
sel = view.sel()
|
||||
window.run_command("copy")
|
||||
|
||||
# Modify the command based on the lines
|
||||
selected = sel[0]
|
||||
lines = view.substr(view.line(selected)).split("\n")
|
||||
first_line = lines[0].lstrip()
|
||||
starts_with_comment = first_line.startswith("#")
|
||||
|
||||
if len(lines) == 1 and not starts_with_comment:
|
||||
command = view.substr(selected) if selected else first_line
|
||||
else:
|
||||
comment = first_line if starts_with_comment else "#"
|
||||
command = f"checkpoint_paste({arg_str}) {comment} ({len(lines)} lines)"
|
||||
|
||||
# Clear selection and put cursor back to the start
|
||||
pos = sel[0].begin()
|
||||
sel.clear()
|
||||
sel.add(sublime.Region(pos))
|
||||
|
||||
send_terminus_command(command)
|
||||
|
||||
|
||||
class ManimRunScene(sublime_plugin.TextCommand):
|
||||
def run(self, edit):
|
||||
view = self.view
|
||||
window = view.window()
|
||||
window.run_command("save")
|
||||
command, enter = get_command(view, window)
|
||||
# If one wants to run it in a different terminal,
|
||||
# it's often to write to a file
|
||||
sublime.set_clipboard(command + " --prerun --finder -w")
|
||||
|
||||
timeout = ensure_terminus_tab_exists()
|
||||
sublime.set_timeout(
|
||||
lambda: send_terminus_command(command, enter=enter),
|
||||
timeout
|
||||
)
|
||||
|
||||
if enter:
|
||||
# Keep cursor where it started
|
||||
sublime.set_timeout(
|
||||
lambda: threading.Thread(target=self.focus_sublime).start(),
|
||||
1000
|
||||
)
|
||||
else:
|
||||
# Put cursor in terminus window
|
||||
sheet = find_terminus_sheet()
|
||||
if sheet is not None:
|
||||
window.focus_view(sheet.view())
|
||||
|
||||
def focus_sublime(self):
|
||||
cmd = "osascript -e 'tell application \"Sublime Text\" to activate'"
|
||||
sp.call(cmd, shell=True)
|
||||
|
||||
|
||||
class ManimExit(sublime_plugin.TextCommand):
|
||||
def run(self, edit):
|
||||
send_terminus_command("\x03quit\n", center=False)
|
||||
time.sleep(0.01)
|
||||
send_terminus_command("", clear=False, center=True, enter=False)
|
||||
|
||||
|
||||
class ManimCheckpointPaste(sublime_plugin.TextCommand):
|
||||
def run(self, edit):
|
||||
checkpoint_paste_wrapper(self.view)
|
||||
|
||||
|
||||
class ManimRecordedCheckpointPaste(sublime_plugin.TextCommand):
|
||||
def run(self, edit):
|
||||
checkpoint_paste_wrapper(self.view, arg_str="record=True")
|
||||
|
||||
|
||||
class ManimSkippedCheckpointPaste(sublime_plugin.TextCommand):
|
||||
def run(self, edit):
|
||||
checkpoint_paste_wrapper(self.view, arg_str="skip=True")
|
||||
|
||||
|
||||
class OpenMirroredDirectory(sublime_plugin.TextCommand):
|
||||
def run(self, edit):
|
||||
window = self.view.window()
|
||||
path = window.extract_variables()["file_path"]
|
||||
new_path = os.path.join(
|
||||
path.replace("_", "").replace(
|
||||
"/Users/grant/cs/videos",
|
||||
R"/Users/grant/3Blue1Brown Dropbox/3Blue1Brown/videos"
|
||||
),
|
||||
window.extract_variables()["file_name"].replace(".py", ""),
|
||||
)
|
||||
print(new_path)
|
||||
sp.call(["open", "-R", new_path])
|
||||
|
||||
|
||||
class CommentFold(sublime_plugin.TextCommand):
|
||||
def run(self, edit):
|
||||
view = self.view
|
||||
regions = view.sel()
|
||||
regions_to_fold = []
|
||||
for region in regions:
|
||||
reg_str = view.substr(region)
|
||||
|
||||
lines = reg_str.split("\n")
|
||||
view_index = region.begin()
|
||||
|
||||
indent_level = None
|
||||
last_full_line_end = view_index
|
||||
last_comment_line_end = None
|
||||
last_line_was_comment = False
|
||||
for line in lines:
|
||||
line_end_point = view_index + len(line)
|
||||
if line.lstrip().startswith("#"):
|
||||
if indent_level is None:
|
||||
indent_level = len(line) - len(line.lstrip())
|
||||
if len(line) - len(line.lstrip()) == indent_level and not last_line_was_comment:
|
||||
if last_comment_line_end:
|
||||
regions_to_fold.append(sublime.Region(
|
||||
last_comment_line_end,
|
||||
last_full_line_end,
|
||||
))
|
||||
last_comment_line_end = line_end_point
|
||||
else:
|
||||
last_line_was_comment = False
|
||||
if line.strip():
|
||||
last_full_line_end = line_end_point
|
||||
view_index = line_end_point + 1
|
||||
if last_comment_line_end:
|
||||
regions_to_fold.append(sublime.Region(
|
||||
last_comment_line_end, region.end(),
|
||||
))
|
||||
self.view.fold(regions_to_fold)
|
Loading…
Add table
Reference in a new issue