Skip to content

Vibe coding a plugin? 😎 Teach your agent Modlumi first. Works with Claude Code, Codex, Cursor and more.

PluginContext

What a plugin’s entry module receives in its exported activate function:

export function activate(context) {
context.registerCommand({ id: "hello", title: "Say Hello", run: () => context.showMessage("Hello") });
}

A plugin is a folder with a plugin.json manifest and its entry module, index.js by default. Its module stays loaded while Modlumi runs, so module-level variables keep their values between commands. When the user turns the plugin off in the Extensions window, Modlumi calls the module’s exported deactivate(), if it has one, and then removes the plugin’s commands and listeners.

readonly documentData: DocumentData

The plugin’s value stored in the open model.


readonly entityData: EntityData

The plugin’s values stored on faces, edges, groups, component instances, definitions and materials.


readonly features: readonly PluginFeature[]

The parts of the plugin API added within its version that this Modlumi has, such as "network" or "mergeVertices". They depend on the build too: one that can’t send requests lacks "network", whatever the user granted. A plugin that needs one lists it under "requires" in plugin.json, so an older Modlumi refuses it with a clear reason; one that can do without checks here.


readonly files: PluginFiles

File dialogs, the files the user picks in them, and the plugin’s data folder.


readonly id: string

The plugin’s id from plugin.json.


readonly name: string

The plugin’s name from plugin.json.


readonly permissions: PluginPermissions

What the user granted of the permissions plugin.json asks for. Calls that need a permission the plugin lacks throw an Error named "PermissionError".


readonly selectedItems: readonly string[]

The ids of the plugin’s pickable items the user selected, in the order selected.


readonly settings: PluginSettings

The plugin’s settings for the current user.


readonly version: string

The plugin’s version from plugin.json.


readonly viewport: Viewport

The viewport of the desktop window: its live camera and size.

addOverlay(draw): () => void

Draws over the viewport on every frame, whichever tool is selected, beneath the selected tool’s preview. draw describes the drawing with the same view as ToolDefinition.draw, in model coordinates, or in screen pixels with screen: true. It only reads the model, and must be synchronous and quick, since it runs every frame. An overlay that throws is removed, with a message; the plugin’s other overlays keep drawing. Returns a function that removes the overlay.

(view) => void

() => void

// Marks the origin with a red cross.
context.addOverlay((view) => {
view.lines([[-0.5, 0, 0], [0.5, 0, 0], [0, -0.5, 0], [0, 0.5, 0]], { color: { r: 0.85, g: 0.2, b: 0.2 }, width: 2 });
});

addPanel(panel): Panel

Adds a dockable panel, listed in the Window menu, where the user opens and closes it. Panels start closed; Modlumi remembers which ones the user left open and where. Adding an id again replaces its definition.

const state = { width: 1, steps: 12 };
context.addPanel({
id: "stairs",
title: "Stairs",
draw(ui) {
state.width = ui.length("Width", state.width);
state.steps = ui.integer("Steps", state.steps, { min: 1 });
},
});

PanelDefinition

Panel


addPickableItems(definition): () => void

Lets the user select the plugin’s own items, such as building levels, with the Select tool, and delete them with Delete, like model entities. items runs every frame, like an overlay, and returns each item’s id and the lines and points the user picks it by, in model coordinates; draw the items themselves with an overlay. Selected items show in the selection’s color. onDelete receives the ids of the deleted items inside one operation, so storing the items in documentData makes deleting them undoable. Returns a function that removes the items.

PickableItemsDefinition

() => void

context.addPickableItems({
items: () => marks.map((mark) => ({ id: mark.id, points: [mark.position] })),
onDelete(ids) {
marks = marks.filter((mark) => !ids.includes(mark.id));
context.documentData.set(marks);
},
});

addPropertiesSection(section): void

Adds a section to Properties, shown while the selection is one group or component instance holding the plugin’s PluginContext.entityData. This is how a plugin lets the user re-edit objects it made:

context.addPropertiesSection({
id: "box",
title: "Box",
draw(ui, container) {
const size = context.entityData.get(container);
const changed = ui.length("Size", size);
if (changed !== size) rebuild(container, changed);
},
});

PropertiesSectionDefinition

void


addSnapProvider(provide): () => void

(viewport) => Iterable<SnapTarget, any, any> | null | undefined

() => void


alert(message, options?): void

Shows a message in a modal dialog with an OK button.

unknown

string

void


animate(frame): number

Calls frame once every frame, with the seconds since the animation started and since the previous frame, until it returns false or clearTimer stops it.

(elapsed, delta) => unknown

number

// Shows a countdown in the status message for three seconds.
context.animate((elapsed) => {
tool.status = `Done in ${Math.ceil(3 - elapsed)}s`;
return elapsed < 3;
});

clearTimer(id): void

Stops a timer or animation; ids of stopped ones are ignored.

number

void


confirm(message, options?): void

Asks a question in a modal dialog with confirm and cancel buttons.

unknown

ConfirmOptions

void


createImage(source): PluginImage

Makes an image from the bytes of a PNG or JPEG file, like loadImage, or from pixels: four bytes of red, green, blue and alpha each, in rows from the top.

ArrayBuffer | Uint8Array<ArrayBufferLike> | { height: number; pixels: ArrayBuffer | Uint8Array<ArrayBufferLike>; width: number; }

PluginImage

// A 2 by 1 image, red then half-transparent blue, saved as a PNG the user chooses.
const image = context.createImage({ width: 2, height: 1, pixels: new Uint8Array([255, 0, 0, 255, 0, 0, 255, 128]) });
const file = await context.files.save({ name: "swatch.png", filters: [{ name: "PNG image", extensions: ["png"] }] });
file?.writeBytes(image.toPng());

fetch(url, options?): Promise<FetchResponse>

Sends a request to a host the plugin’s "network" permission lists and the user granted, and resolves with the response, or rejects with a TypeError when the request fails. Only http and https URLs work, redirects come back as responses rather than being followed, and a plugin may have 8 requests in flight. A command can await it: its run finishes while the request is out, and continues once the response arrives. When another model opens before the response arrives, the promise rejects with a ReferenceError instead, and the code that runs for it, such as a catch or finally, can’t reach the model open now: reading or editing it throws a ReferenceError too. Restarting or turning off the plugin cancels its requests. Desktop builds on Linux and macOS can make requests; others throw.

string

FetchOptions

Promise<FetchResponse>

const response = await context.fetch("https://api.example.com/prices?part=beam");
if (response.ok) {
context.showMessage(`Beams cost ${response.json().price}`);
}

loadImage(path): PluginImage

Loads a PNG or JPEG file from the plugin folder, for overlays and previews to draw with ToolView.image. A plugin’s images may hold 64 million pixels together.

string

PluginImage


onActiveContextChanged(listener): () => void

Calls listener after the user enters or leaves a group or component. Returns a function that removes it.

ChangeListener

() => void


onCameraChanged(listener): () => void

Calls listener after the camera moved: orbiting, panning, zooming, a standard view or Viewport.setCamera. While the user orbits it runs every frame, so keep it quick. Returns a function that removes it.

ChangeListener

() => void


onDocumentClosed(listener): () => void

Calls listener after New or Open replaced the open document, just before the PluginContext.onDocumentOpened listeners. The previous document is already gone. Returns a function that removes it.

ChangeListener

() => void


onDocumentOpened(listener): () => void

Calls listener after New or Open replaced the open document, once the new document is open. Returns a function that removes it.

ChangeListener

() => void


onModelChanged(listener): () => void

Calls listener after the model changes: an edit, an undo or a redo, by the user, a script or a plugin. Returns a function that removes it.

Listeners run once per frame for everything that changed during it, in this order: document closed, document opened, active context, selection, model. They receive no arguments; read what they need from activeModel. Listeners only read, so the user can keep drawing: editing the model or the selection, undo and redo throw ModelError. Use a command for edits. A listener that runs for more than a second is stopped and its plugin restarted. Listener errors are logged and never stop other listeners.

context.onModelChanged(() => {
const faces = modlumi.activeModel.entities.ofType("Face").length;
console.log(`${faces} faces`);
});

ChangeListener

() => void


onSelectionChanged(listener): () => void

Calls listener after the selection changes. Returns a function that removes it. See PluginContext.onModelChanged for when listeners run.

ChangeListener

() => void


onViewResized(listener): () => void

Calls listener after the viewport changed size. Returns a function that removes it.

ChangeListener

() => void


registerCommand(command): void

Adds a command to the plugin’s submenu in the Extensions menu. Registering an id again replaces that command.

CommandDefinition

void


registerExporter(exporter): void

Adds File > Export name, which asks the user where to save a file with one of the extensions and passes it to export, which writes the model to it. export must be synchronous, only reads the model, and needs no permission to write the file.

ExporterDefinition

void


registerImporter(importer): void

Adds File > Import name, which asks the user for a file with one of the extensions and passes it to import, inside one undoable operation; dropping such a file on the window imports it too. import must be synchronous, and needs no permission to read the file. Modlumi keeps .modl and .skp to itself. When the plugin restarts or another model opens before the import runs, it is dropped.

ImporterDefinition

void

context.registerImporter({
id: "points",
name: "Point List",
extensions: ["xyz"],
import(file) {
const points = file.readText().trim().split("\n").map((line) => line.split(/\s+/).map(Number));
for (const point of points) modlumi.activeModel.activeEntities.addGuidePoint(point);
},
});

registerTool(tool): PluginTool

Adds an interactive tool, listed in the plugin’s submenu of the Extensions menu, where choosing it selects it, and optionally in the toolbar. Returns its handle. Tools and commands share ids.

const tool = context.registerTool({
id: "mark",
title: "Mark Points",
activate: () => { tool.status = "Click to add a guide point."; },
onMouseUp(event) {
if (event.point) model.operation("Mark", () => model.activeEntities.addGuidePoint(event.point.position));
},
});

ToolDefinition

PluginTool


setInterval(callback, interval): number

Calls callback every interval milliseconds, at most once a frame, until clearTimer.

() => unknown

number

number


setTimeout(callback, delay?): number

Calls callback once, between frames, after at least delay milliseconds, and returns an id for clearTimer. Timers stop when the plugin is turned off or reloads.

Timer callbacks may edit the model inside model.operation(); an edit ends the gesture of the active tool, as a command does. Callbacks must be synchronous; one that throws stops its timer.

() => unknown

number

number


showDialog(dialog): Dialog

Shows a dialog, or brings back the open one with the same id with this definition.

DialogDefinition

Dialog


showMessage(text): void

Shows a short message to the user, such as the result of a command.

string

void