Skip to content

Add import or export actions to the File menu

Register custom File menu actions and handle file selection, asynchronous work and export errors.

We will add two menu entries: one reads a local project file, and the other saves a model’s JSON export. The editor provides a place in the menu, availability controls and support for awaiting the handler. The plugin handles file selection, input validation and downloading the result.

First, make sure you understand the difference between public and internal entry points. For testing, prepare a copy of a small project without real block structures.

Workflow and data formats

Download the complete file-actions.js plugin. After installing and restarting, these entries appear:

  • File → Import → Add content from a project file adds the contents of a .bdengine file to the current project.
  • File → Export → Download the JSON model export obtains Minecraft export data and saves it as .json.

These are two different data flows. The downloaded JSON export is not a project file and is not intended to be imported back through this entry. The example does not register a new extension with the editor’s file-opening system or add drag-and-drop handling.

Public operations are called through editorAPI. To check that the user has not switched projects or modes while choosing a file, the example reads editor.objects and mode flags. This is an internal dependency that must be checked separately when the editor is updated.

Import and export use the same options structure:

const action = editorAPI.addButtonImportMenu({
id: 'my-plugin.import-project',
title: 'Add content from a project file',
icon: 'icon-file-input',
onClick: async (event) => {
await importProject();
}
});

importProject represents your handler here; the complete version is in the downloadable file. id, a nonempty title and an onClick function are required. icon is a single icon CSS class and can be omitted. The optional enabled property sets the initial availability.

Give id a prefix unique to your plugin. Registering the same id again in the same section updates the existing entry instead of creating another. The same id in import and export belongs to two different sections.

The entry appears after the built-in actions and a divider. Its label is treated as plain text. See the contracts for adding an import action and adding an export action.

Availability and asynchronous work

The returned handle supports two methods:

action.setEnabled(false);
action.setEnabled(true);
action.remove();

setEnabled requires a boolean and returns the same handle. remove() removes the entry and returns true; calling it again returns false. Removing an entry does not cancel file reading or other asynchronous work that is already running.

While onClick is running, the editor prevents another invocation of that entry. For this protection to cover asynchronous work, return the Promise or use async and await:

// Correct: the menu waits for the operation to finish.
onClick: () => importProject()
// Incorrect: the Promise is not returned, so the menu considers the handler finished.
onClick: () => { importProject(); }

In the complete example, a shared busy flag and setEnabled(false) temporarily disable both entries so the plugin’s import and export operations do not overlap. finally restores availability after success, cancellation or an error. Other editor commands remain available.

The menu invokes the handler before its first await. Open the file picker immediately inside the handler: waiting for a network request first can lose the user activation the browser requires to open the picker.

Handle the file yourself

The example creates a hidden input with type="file" and accept=".bdengine", then calls click(). The change event provides the selected file; cancel finishes the operation without one. The temporary element is removed after either event. Cancellation is not an error.

accept filters the picker; it does not validate file contents. The handler also checks the extension, a nonzero size and a 10 MB limit. That limit belongs to the example plugin, not to a promised BDEngine limit. The file is read as bytes:

const bytes = new Uint8Array(await file.arrayBuffer());
await editorAPI.mergeContent(bytes, true);

BDEngine’s importer validates the project itself. Renaming arbitrary JSON to .bdengine does not make it a valid project. The example targets ordinary modeling mode; real block structures cannot be added this way. Files containing custom models require the complete project container, not just scene text extracted from it.

Handle arrayBuffer exceptions, JSON parsing errors when reading JSON yourself, missing export results and cancellation. The example shows errors to the user and writes them to the console. finally restores the menu state.

Get or add content

mergeContent(content, decode) adds content to the current project while keeping its existing objects. With decode: true, it reads a container or a supported encoded representation. With decode: false, it expects a BDEngine scene JSON string, not a parsed object or SNBT:

await editorAPI.mergeContent(JSON.stringify(projectScene), false);

In this snippet, projectScene is already prepared BDEngine scene data. For its structure and limits, see working with project data.

The editorAPI.mergeContent wrapper does not pass through the internal importer’s boolean result. A fulfilled await does not prove that objects were added: the editor can handle an error itself and display a message. Inspect the scene result; do not show an unconditional “Import successful” message just because the Promise fulfilled.

Minecraft export data uses a different method:

const content = await editorAPI.exportToJSON(false);
if (!content) throw new Error('No export was returned.');
const text = JSON.stringify(content, null, 2);

false turns off the full-export request in the export window’s settings, but it does not take the editor out of Animator: the result in Animator can still have type: "full". The complete example therefore requires ordinary modeling mode before exporting and checks content.type === 'modelOnly' afterwards. If the mode or result type is unsuitable, it shows a message and does not download a file.

The method uses the editor’s export window, changes its type and selected version, and shows it; it is not background project serialization. The complete example saves text using a Blob, a temporary URL and a link with download, then releases the URL.

The result has no HTTP content wrapper, and the [BDESERVERTAG] marker is not replaced with a tag value. For failure conditions and parameters, see getting export data.

Test the menu and invalid input

  1. Find both entries after restarting. Make sure opening the menu repeatedly does not create duplicates.
  2. Cancel the file picker. Both entries should become available again.
  3. Try an empty file, the wrong extension and a file larger than 10 MB. The plugin should report the problem before calling the importer.
  4. Add a small model from a real .bdengine file. Existing objects should remain; check the new ones in the object tree, rather than relying on an empty error console.
  5. Switch projects through another available editor action while choosing a file. The old read operation must not begin importing into the new project root.
  6. In ordinary modeling mode, download the export, open it as JSON and check type: "modelOnly" and passengers. Then run the entry in Animator: the example should ask you to switch to modeling without starting an export. Unprepared textures or export errors must not leave the menu disabled.
  7. In your own version, keep the handle and test remove(): only your entry should disappear.

The cancel event and downloads depend on the browser’s file interface. Before distributing the plugin, test both actions in the target web editor and BDEngine App.