Building Plugins
You can build your own plugins to support your own workflow, and even share it to help others!
You can also use the Yeoman Generator to accelerate your plugin development process. The generator will generate the necessary files that are described here, as well as boilerplate code to help you get started!
Core Concepts
Before beginning, it's crucial to understand the core concepts unique to plugin development, which differ from those in building Lenses. Grasping these fundamentals is key to comprehending the structure of plugins.
As you try these various capabilities, you can use console.log
to print messages to the Lens Studio Logger
panel.
The Editor namespace
The Editor
namespace contains the majority of the Editor API. For example, you can access camera type strings via Editor.Components.CameraType.Orthographic
; or create a new Path
object by new Editor.Path()
.
Model object
The model
object is a core concept in the plugin development environment. It serves as a central point for accessing key elements such as the scene, project, and assetManager.
The model
object encapsulates the data model representing a Lens Studio project, and project manipulation. It brings together environment entities and functionalities that are essential for developing plugins. It plays a role analogous to the "Model" component found in Model-View-Controller architectural patterns, containing both data and business logic.
In order to get the model object, which many key objects are stored within, you need the pluginSystem object
which is being passed into the constructor of the plugin class, along with the ID of the model component (which can be accessed through the Editor
namespace)
const model = pluginSystem.findInterface(Editor.Model.IModel);
const assetManager = model.project.assetManager;
const scene = model.project.scene;
Scene object
The scene object handles access to the project scene. It allows interacting with scene objects programmatically. It hosts key functions such as findComponents(entityType: string): Editor.Components.Component[]
, sceneObjects : Editor.Model.SceneObject[]
, rootSceneObjects: Editor.Model.SceneObject[]
and many more.
For example, to add a new scene object:
const newSceneObject = scene.createSceneObject('name');
AssetManager object
Whenever you need to interface with the assets in the project, you need the assetManager object. For example:
const assetManager = model.project.assetManager;
//import external file as assets
await assetManager.importExternalFileAsync(
path,
'',
Editor.Model.ResultType.Packed
);
//create a new material
assetManager.createNativeAsset(
'Material',
materialFileName,
new Editor.Path(materialFilePath)
);
//find a copy from the existing assets
assetManager.findImportedCopy(absolutePath);
Plugin Directory Structure
Within a single plugin modules directory, you may have multiple plugins (as denoted by a folder and a module.json file which specifies the entry script). When you load a plugin by adding a plugin directory in the Plugin Manager, each plugin will appear with its own checkbox. When this checkbox is enabled, the plugin will be enabled for the entire Lens Studio application.
Here is an example plugins directory containing two plugins (a CoreService plugin and a Preset plugin):
- 📁 PluginModulesDirectory
- 📁 CoreServicePlugin
- 📄 main.js
- 📄 module.json
- 📁 PresetPlugin
- 📄 icon.svg
- 📄 main.js
- 📄 module.json
- 📁 Resources
- 📄 example_image.png
- 📄 example_material.ss_graph
- 📁 CoreServicePlugin
Apart from module.json
, the naming of all other files and directories listed above is flexible and can be changed.
In the example above, you would add the PluginsDirectory to Lens Studio, and you will see the CoreServicePlugin and PresetPlugin become available. (You might have to close and re-open the Plugin Manager for the changes to take effect).
Entry Script
The module.json
file is the plugin's entry point. The naming is preset so Lens Studio could find it. It should include a JavaScript object that has a key called main, pointing to your entry JavaScript file. Access to certain modules needs permission, such as network, filesystem, shell, and secure_local_storage. A popup will be shown at the loading of the plugin asking the user to grant the needed permissions.
{
"name": "My Plugin Name",
"main": "main.js",
"permissions": [
"network",
"filesystem",
"shell"
// ...anything else you need
]
}
You can add additional files beyond the entry script. Using separate files can help modularize and organize your code. Interaction between scripts uses standard ECMA-style import/export schemes.
Class Structure
Import
Your plugin class should extend one of the base classes:
- PanelPlugin
- DialogPlugin
- ScreenPlugin
- Preset
- CoreService
Ensure that these base classes are imported from their respective modules, which are LensStudio:PanelPlugin
, LensStudio:DialogPlugin
, LensStudio:ScreenPlugin
, LensStudio:Preset
, and LensStudio:CoreService
.
Export
In accordance with the configuration specified in 'module.json', ensure that your plugin class is properly exported in the entry script. It's important to note that multiple plugin classes can be exported within a single entry script.
//main.js minimal structure
import PanelPlugin from 'LensStudio:PanelPlugin';
export class YourPluginName extends PanelPlugin {
static descriptor() {
// ...
}
constructor(pluginSystem) {}
}
Aside from extending the appropriate base class, the following functions are required in your class, no matter the type of the plugin.
Descriptors
The static function static descriptor( ) : void
contains fields needed to populate the plugin descriptor (refer to table below). All plugins are required to populate the id, interfaces, name, description fields.
For Preset plugins, additional fields are required: icon
, section
, and entityType
.
If you include unsupported fields in the descriptor, an error will not occur but nothing will happen with them. If you miss a field or populate a field with an incorrect type, the descriptor will not be read and the plugin will not be processed.
id: String
- Needs to be a unique identifier, suggested to follow Reverse Domain Name
interfaces: String[]
- The IDs of the interfaces that the plugin depends on.
name: String
- A more readable name for the plugin
description: String
- A more verbose description of the plugin
dependencies: String[]
- A list of modules that this plugin relies on to function properly.
Example list of modules are:
Editor.IContextActionRegistry
Editor.IEntityPicker
Editor.IAuthorization
Editor.Model.IEntityPrototypeRegistry
Editor.Model.IEntityRegistry
Editor.Model.IModel
LensStudio:AssetLibrary.IAssetLibraryProvider
LensStudio:Ui.IGui
Note:Model.IModel
is not needed for Preset, since it’s always used, and thus automatically added.
- A list of modules that this plugin relies on to function properly.
Example list of modules are:
icon: Icon
- Preset only. This is the icon that appears in the “+” menu. Save in folder as icon.svg (currently svg is the only supported file type)
section: String
- Preset only. You may specify any string and the preset will be shown in the corresponding section in the Scene Hierarchy or Asset Browser “+” dropdown menu. E.g., General, Effects, Face, Physics.
entityType: String
- Preset only. Currently supported entities are:
Asset
and its derived typesComponent
and its derived typesSceneObject
- Preset only. Currently supported entities are:
Take a look at the Editor API to learn more.
Constructor
Within the constructor constructor(pluginSystem) : void
of your plugin class, you must call the base type constructor first, i.e., super(pluginSystem)
. In addition to this, you can place any startup logic for the plugin in this function. The passed-in parameter pluginSystem will become the bridge towards the needed objects to carry out the operation of your plugin, See the Core Concepts section for specific information.
Using TypeScript
Plugins are meant to be tools to help you work more efficiently. Sometimes it can get a bit tricky to manage the complexity of your plugin. TypeScript can help you manage the complexity of your plugin by providing you with type checking and autocompletion. Although the plugin system doesn't support interpreting TypeScript files directly, you can still use TypeScript to write your plugin and then compile it to JavaScript. Here is how you can do it:
Make sure you have TypeScript installed
If your system doesn't have TypeScript installed, you can install it by referring to this link.
Create a tsconfig.json file
Inside of your plugin directory, create a tsconfig.json
file with the following content:
{
"compilerOptions": {
"target": "ES2019",
"module": "ES2020",
"rootDir": "./src",
"outDir": "./dist",
"strict": true
},
"include": ["src/**/*"]
}
Feel free to edit and add more options to the tsconfig.json
file. You can find more information about the options here.
Create a src directory
Inside of your plugin directory, create a src
directory. This is where you will put your TypeScript files.
Compile your TypeScript files
Run the following command in your terminal to compile your TypeScript files:
tsc
or if you want to watch for changes and compile automatically, you can run:
tsc -w
This will compile your TypeScript files and output the compiled JavaScript files in the dist
directory.
Set up the entry script
In your module.json
file, make sure to point to the compiled JavaScript file as the entry script. For example, if your entry script is main.ts
, which gets compiled as main.js
, you should point to dist/main.js
in the module.json
file.
(Optional) Add TypeScript definitions
To add TypeScript definitions for the Lens Studio Editor API:
- Create a
typings
directory in your project. - Download the latest Type Definitions file for the Editor Scripting API from here.
- Place the downloaded file (typically named as
editor.d.ts
) in thetypings
directory. - In your
tsconfig.json
, add the path to this file in the"include"
array:
{
"include": ["src/**/*", "typings/**/*"]
}
This will allow TypeScript to recognize and provide code completions for Lens Studio API functions and types in your code.