Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
575b4a6
Init export scene command
Splines Nov 10, 2024
f54aff8
Add video quality and fps quick pick
Splines Nov 10, 2024
d5274d4
Use more advanced multi-step quick pick
Splines Nov 10, 2024
495b9a9
Fix should resume & outsource to multi step file
Splines Nov 10, 2024
0317404
Add file picker as last step
Splines Nov 10, 2024
4d915d3
Init codelens for export scene
Splines Nov 10, 2024
1a804bb
Extract scene name and pass to export
Splines Nov 10, 2024
e74fd5f
Handle terminal wait
Splines Nov 10, 2024
5346380
Also pick filename
Splines Nov 10, 2024
8f954b0
Rename function
Splines Nov 10, 2024
fcc8b0d
Add docstrings to export file
Splines Nov 10, 2024
37d6b9d
Add docstring to waitNewTerminalDelay()
Splines Nov 10, 2024
aba26a1
Add docstring explaining copied file
Splines Nov 10, 2024
b991cc3
Put utils files in separate folder
Splines Nov 10, 2024
36a2713
Validate more cases in user input
Splines Nov 10, 2024
47df5cc
Change Codelens emoji
Splines Nov 10, 2024
811ee2a
Merge branch 'main' into feature/export
Splines Nov 17, 2024
cf7ff33
Rename codelens to "Generate command to export scene"
Splines Nov 17, 2024
66920c5
Break up long line
Splines Nov 17, 2024
f43c92a
Break up another long line
Splines Nov 17, 2024
fabc386
Extract scene name finding to own method
Splines Nov 17, 2024
e42f3f3
Expose export scene command to command palette
Splines Nov 17, 2024
75a6c27
Save active file before export
Splines Nov 17, 2024
7be1cd0
Copy export command to clipboard & show message
Splines Nov 17, 2024
e48f518
Adjust codelens tooltip
Splines Nov 17, 2024
074a659
Simplify export command names
bhoov Dec 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"ansi",
"Ansi",
"autoplay",
"cmds",
"github",
"ipython",
"Keybord",
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
"command": "manim-notebook.recordLogFile",
"title": "Record Log File",
"category": "Manim Notebook"
},
{
"command": "manim-notebook.exportScene",
"title": "Export current scene as CLI command",
"category": "Manim Notebook"
}
],
"keybindings": [
Expand Down
252 changes: 252 additions & 0 deletions src/export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import * as vscode from 'vscode';
import { QuickPickItem, window } from 'vscode';
import {
MultiStepInput, toQuickPickItem, toQuickPickItems,
shouldResumeNoOp
} from './utils/multiStepQuickPickUtil';
import { findClassLines, findManimSceneName } from './pythonParsing';
import { Logger, Window } from './logger';
import { waitNewTerminalDelay } from './manimShell';


class VideoQuality {
static readonly LOW = new VideoQuality('Low Quality (480p)', '--low_quality');
static readonly MEDIUM = new VideoQuality('Medium Quality (720p)', '--medium_quality');
static readonly HIGH = new VideoQuality('High Quality (1080p)', '--hd');
static readonly VERY_HIGH = new VideoQuality('Very High Quality (4K)', '--uhd');

private constructor(public readonly name: string, public readonly cliFlag: string) { }

/**
* Returns the names of all VideoQuality objects.
*/
static names(): string[] {
return Object.values(VideoQuality).map((quality) => quality.name);
}

/**
* Returns the VideoQuality object that has the given name.
*/
static fromName(name: string): VideoQuality {
return Object.values(VideoQuality).find((quality) => quality.name === name);
}
}

interface VideoSettings {
sceneName: string;
quality: string;
fps: string;
fileName: string;
folderPath: string;
}

/**
* Guides the user through a multi-step wizard to configure the export options
* for a scene, i.e. quality, fps, filename, and folder path. The final command
* is then pasted to a new terminal where the user can run it.
*
* @param sceneName The name of the Manim scene to export. Optional if called
* from the command palette. Not optional if called from a CodeLens.
*/
export async function exportScene(sceneName?: string) {
await vscode.commands.executeCommand('workbench.action.files.save');

// Command called via command palette
if (sceneName === undefined) {
const editor = window.activeTextEditor;
if (!editor) {
return Window.showErrorMessage(
"No opened file found. Please place your cursor in a Manim scene.");
}

const sceneClassLine = findManimSceneName(editor.document, editor.selection.start.line);
if (!sceneClassLine) {
return Window.showErrorMessage("Place your cursor in a Manim scene.");
}

sceneName = sceneClassLine.className;
}

const QUICK_PICK_TITLE = "Export scene as video";

/**
* Lets the user pick the quality of the video to export.
*/
async function pickQuality(input: MultiStepInput, state: Partial<VideoSettings>) {
const qualityPick = await input.showQuickPick({
title: QUICK_PICK_TITLE,
step: 1,
totalSteps: 3,
placeholder: "Select the quality of the video to export",
items: toQuickPickItems(VideoQuality.names()),
shouldResume: shouldResumeNoOp
});
state.quality = VideoQuality.fromName(qualityPick.label).cliFlag;

return (input: MultiStepInput) => pickFps(input, state);
}

/**
* Lets the user pick the frames per second (fps) of the video to export.
*/
async function pickFps(input: MultiStepInput, state: Partial<VideoSettings>) {
const fps = await input.showInputBox({
title: QUICK_PICK_TITLE,
step: 2,
totalSteps: 3,
placeholder: "fps",
prompt: "Frames per second (fps) of the video",
value: typeof state.fps === 'string' ? state.fps : "30",
validate: async (input: string) => {
const fps = Number(input);
if (isNaN(fps) || fps <= 0) {
return "Please enter a positive number.";
}
if (input.includes(".")) {
return "Please enter an integer number.";
}
return undefined;
},
shouldResume: shouldResumeNoOp,
});
state.fps = fps;

return (input: MultiStepInput) => pickFileName(input, state);
}

/**
* Lets the user pick the filename of the video to export. The default value
* is the name of the scene followed by ".mp4".
*
* It is ok to not append `.mp4` here as Manim will also do it if it is not
* present in the filename.
*/
async function pickFileName(input: MultiStepInput, state: Partial<VideoSettings>) {
const fileName = await input.showInputBox({
title: QUICK_PICK_TITLE,
step: 3,
totalSteps: 3,
placeholder: `${sceneName}.mp4`,
prompt: "Filename of the video",
value: state.fileName ? state.fileName : `${sceneName}.mp4`,
validate: async (input: string) => {
if (!input) {
return "Please enter a filename.";
}
if (/[/\\]/g.test(input)) {
return "Please don't use slashes."
+ " You can specify the folder in the next step.";
}
if (/[~`@!#$§%\^&*+=\[\]';,{}|":<>\?]/g.test(input)) {
return "Please don't use special characters in the filename.";
}
if (input.endsWith(".")) {
return "Please don't end the filename with a dot.";
}
return undefined;
},
shouldResume: shouldResumeNoOp,
});
state.fileName = fileName;

return (input: MultiStepInput) => pickFileLocation(input, state);
}

/**
* Lets the user pick the folder location where the video should be saved.
*/
async function pickFileLocation(input: MultiStepInput, state: Partial<VideoSettings>) {
const folderUri = await window.showOpenDialog({
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
openLabel: "Select folder",
title: "Select folder to save the video to",
});
if (folderUri) {
state.folderPath = folderUri[0].fsPath;
}
}

/**
* Initiates the multi-step wizard and returns the collected inputs
* from the user.
*/
async function collectInputs(): Promise<VideoSettings> {
const state = {} as Partial<VideoSettings>;
state.sceneName = sceneName;
await MultiStepInput.run((input: MultiStepInput) => pickQuality(input, state));
return state as VideoSettings;
}

const settings = await collectInputs();
if (!settings.quality || !settings.fps || !settings.fileName || !settings.folderPath) {
Logger.debug("⭕ Export scene cancelled since not all settings were provided");
return;
}

const editor = window.activeTextEditor;
if (!editor) {
return Window.showErrorMessage(
"No opened file found. Please place your cursor at a line of code.");
}

const exportCommand = toManimExportCommand(settings, editor);

await vscode.env.clipboard.writeText(exportCommand);
const terminal = window.createTerminal("Manim Export");
terminal.show();
await waitNewTerminalDelay();
terminal.sendText(exportCommand, false);
Window.showInformationMessage("Export command pasted to terminal and clipboard.");
}

/**
* Converts the given VideoSettings object into a Manim export command.
*
* See the Manim documentation for all supported flags:
* https://3b1b.github.io/manim/getting_started/configuration.html#all-supported-flags
*
* @param settings The settings defined via the multi-step wizard.
* @param editor The active text editor.
* @returns The Manim export command as a string.
*/
function toManimExportCommand(settings: VideoSettings, editor: vscode.TextEditor): string {
const cmds = [
"manimgl", `"${editor.document.fileName}"`, settings.sceneName,
"-w", settings.quality, `--fps ${settings.fps}`,
`--video_dir "${settings.folderPath}"`,
`--file_name "${settings.fileName}"`
];
return cmds.join(" ");
}

/**
* A CodeLens provider that adds a `Export Scene` CodeLens to each Python
* class definition line in the active document.
*/
export class ExportSceneCodeLens implements vscode.CodeLensProvider {

public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken)
: vscode.CodeLens[] {
const codeLenses: vscode.CodeLens[] = [];

for (const classLine of findClassLines(document)) {
const range = new vscode.Range(classLine.lineNumber, 0, classLine.lineNumber, 0);

codeLenses.push(new vscode.CodeLens(range, {
title: "🎞️ Export Manim scene",
command: "manim-notebook.exportScene",
tooltip: "Generate a command to export this scene as a video",
arguments: [classLine.className]
}));
}

return codeLenses;
}

public resolveCodeLens(codeLens: vscode.CodeLens, token: vscode.CancellationToken)
: vscode.CodeLens {
return codeLens;
}
}
17 changes: 15 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import * as vscode from 'vscode';
import { window } from 'vscode';
import { ManimShell, NoActiveShellError } from './manimShell';
import { ManimCell } from './manimCell';
import { ManimCellRanges } from './manimCellRanges';
import { ManimCellRanges } from './pythonParsing';
import { previewCode } from './previewCode';
import { startScene, exitScene } from './startStopScene';
import { exportScene } from './export';
import { Logger, Window, LogRecorder } from './logger';
import { ExportSceneCodeLens } from './export';

export function activate(context: vscode.ExtensionContext) {
// Trigger the Manim shell to start listening to the terminal
Expand Down Expand Up @@ -51,7 +53,17 @@ export function activate(context: vscode.ExtensionContext) {
await LogRecorder.recordLogFile(context);
});

// internal command
// Internal commands
const exportSceneCommand = vscode.commands.registerCommand(
'manim-notebook.exportScene', async (sceneName?: string) => {
Logger.info("💠 Command requested: Export Scene");
await exportScene(sceneName);
});
context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
{ language: 'python' }, new ExportSceneCodeLens())
);

const finishRecordingLogFileCommand = vscode.commands.registerCommand(
'manim-notebook.finishRecordingLogFile', async () => {
Logger.info("💠 Command requested: Finish Recording Log File");
Expand All @@ -65,6 +77,7 @@ export function activate(context: vscode.ExtensionContext) {
exitSceneCommand,
clearSceneCommand,
recordLogFileCommand,
exportSceneCommand,
finishRecordingLogFileCommand
);
registerManimCellProviders(context);
Expand Down
2 changes: 1 addition & 1 deletion src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import { window } from 'vscode';
import { LogOutputChannel } from 'vscode';
import { waitUntilFileExists, revealFileInOS } from './fileUtil';
import { waitUntilFileExists, revealFileInOS } from './utils/fileUtil';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
Expand Down
2 changes: 1 addition & 1 deletion src/manimCell.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as vscode from 'vscode';
import { window } from 'vscode';
import { ManimCellRanges } from './manimCellRanges';
import { ManimCellRanges } from './pythonParsing';

export class ManimCell implements vscode.CodeLensProvider, vscode.FoldingRangeProvider {
private cellStartCommentDecoration: vscode.TextEditorDecorationType;
Expand Down
Loading