Scripting the PlayCanvas Editor


#1

Do you have any plans for allowing a user to write logic for the PlayCanvas designer?

For example:

  • Creating procedural levels that add entities directly in the Designer
  • Extending its functionality e.g. creating a path tool for navigation (camera, entities etc.)

Something like maxscript :slight_smile: only you already have javascript and a scripting system in place.

Thanks for the great work!


Custom editor js command
Editor's gizmos inside an application
#2

Hi @Leonidas

We had ideas and plans long ago to work on Plugin system. But it is something we will have to address somewhere in the future.
But! :slight_smile: there is an API in editor right now, it is not hidden and not minified from users. It is just not cleared up and not documented.

You could have noticed that editor.js is not minified, but only concatenated, so you can have a peek inside.
What to look for:
Editor uses a messaging approach to communicate between parts instead of interfaces. This provides failure tolerant code execution. So main way to do something is using editor.call(method); - this calls some function, with arguments after name if needed. And sometimes returns results.
For example getting asset data object:

var asset = editor.call('assets:get', 42);

This will get an asset object observer.
Object Observer - is an interface to set/get data to. Everything in editor is bound to its changes, so changing that object will be saved on server, updates ui, viewport, history and other relevant bits.
So to change lets say name of it:

asset.set('name', 'name from script');

To get lets say file size:

var size = asset.get('file.size');

Or to get full JSON representation of it:

var json = asset.json();

Entities, Assets, Scene Settings and few other bits in editor are all observer objects.

There are number of different methods, and to get list of them type: editor._hooks. There are 333 methods atm.

assets:create (data, fn, noSelect)
assets:create:css (args)
assets:create:cubemap (args)
assets:create:folder (args)
assets:create:html (args)
assets:create:json (args)
assets:create:material (args)
assets:create:script (args)
assets:create:shader (args)
assets:create:text (args)
assets:cubemaps:prefilter (assetCubeMap, callback)
assets:delete (list)
assets:delete:picker (items)
assets:duplicate (asset)
assets:edit (asset)
assets:filter:search (query)
assets:filter:type (type)
assets:filter:type:disabled (state)
assets:find (fn)
assets:findOne (fn)
assets:fs:delete (assets)
assets:fs:duplicate (assets)
assets:fs:move (assets, assetTo)
assets:fs:paths:patch (data)
assets:get (id)
assets:grid ()
assets:jobs:add (asset)
assets:jobs:convert (asset, options)
assets:jobs:remove (id)
assets:jobs:texture-convert-options (meta)
assets:jobs:thumbnails (source, target)
assets:list ()
assets:map (fn)
assets:model:area (asset, fn)
assets:model:unwrap (asset, args, fn)
assets:model:unwrap:cancel (asset)
assets:model:unwrapping (asset)
assets:panel:currentFolder (asset)
assets:panel:files ()
assets:panel:filter (fn)
assets:panel:filter:default ()
assets:panel:folders ()
assets:panel:get (id)
assets:panel:message (msg)
assets:pipeline:settings (name, value)
assets:progress (progress)
assets:raw ()
assets:registry:bind (assetRegistry, assetTypes)
assets:remove (asset)
assets:scripts:assetByScript (script)
assets:scripts:collide (script)
assets:scripts:list ()
assets:scripts:typeToSubTitle (attribute)
assets:upload:files (files)
assets:upload:picker (args)
assets:upload:script (file)
assets:uploadFile (args, fn)
assets:used:get (id)
assets:used:index ()
attributes.rootPanel ()
attributes:addAssetsList (args)
attributes:addField (args)
attributes:addPanel (args)
attributes:asset:model:nodesPanel ()
attributes:assets:panel ()
attributes:clear ()
attributes:entity.panelComponents ()
attributes:entity:addComponentPanel (args)
attributes:header (text)
attributes:inspect (type, item)
attributes:items ()
attributes:linkField (args)
attributes:reference (args)
attributes:reference:add (args)
attributes:reference:attach (name, target, element)
attributes:reference:template (args)
camera:add (entity)
camera:current ()
camera:depth:pixelAt (camera, x, y)
camera:depth:render (camera)
camera:editor ()
camera:fly:state ()
camera:focus (point, distance)
camera:focus:stop ()
camera:get (name)
camera:history:start (entity)
camera:history:stop (entity)
camera:list ()
camera:pan:start (tap)
camera:remove (entity)
camera:set (entity)
chat:inputField ()
chat:messagesPanel ()
chat:panel ()
chat:post (type, string)
chat:send (message)
chat:sync:msg (data)
chat:sync:typing (data)
chat:typing (state)
chat:unreadCount ()
color:hsl2rgb (h, s, l)
components:convertValue (component, property, value)
components:getDefault (component)
components:list ()
components:schema ()
cursor:clear ()
cursor:set (type)
cursor:text (text)
datetime:convert (date)
designerSettings ()
designerSettings:panel:unfold (panel)
drop:activate (state)
drop:active ()
drop:item (args)
drop:set (type, data)
drop:target (obj)
editor:tips:reset ()
entities:aabb (items)
entities:add (entity)
entities:boundingbox (entity)
entities:boundingbox:entity (entity)
entities:clear ()
entities:clipboard:empty ()
entities:clipboard:get ()
entities:clipboard:set (data)
entities:copy (entity)
entities:delete (items)
entities:duplicate (entities)
entities:fuzzy-search (query, limit)
entities:get (resourceId)
entities:hierarchy ()
entities:list ()
entities:list:byScript (script)
entities:new (raw)
entities:panel:get (resourceId)
entities:panel:highlight (resourceId, highlight)
entities:paste (parent)
entities:remove (entity)
entities:root ()
entities:selectedFirst ()
entities:selection ()
gizmo:collision:visible (state)
gizmo:coordSystem (system)
gizmo:point:create (axis, position, dir)
gizmo:point:recycle (point)
gizmo:rotate:position (x, y, z)
gizmo:rotate:rotation (pitch, yaw, roll)
gizmo:rotate:toggle (state)
gizmo:rotate:visible (state)
gizmo:scale:position (x, y, z)
gizmo:scale:rotation (pitch, yaw, roll)
gizmo:scale:toggle (state)
gizmo:scale:visible (state)
gizmo:snap (state)
gizmo:translate:position (x, y, z)
gizmo:translate:rotation (pitch, yaw, roll)
gizmo:translate:toggle (state)
gizmo:translate:visible (state)
gizmo:type (type)
gizmo:zone:visible (state)
guide:bubble (title, text, x, y, align, parent)
guide:bubble:show (name, bubbleFn, delay, force, callback)
help:controls ()
help:howdoi ()
help:howdoi:popup (data)
help:howdoi:register (data)
help:howdoi:toggle ()
history:add (action)
history:canRedo ()
history:canUndo ()
history:clear ()
history:current (action)
history:list ()
history:redo ()
history:undo ()
history:update (action)
hotkey:alt ()
hotkey:ctrl ()
hotkey:register (name, args)
hotkey:shift ()
hotkey:unregister (name)
images:upload (file, callback, error)
launch (type)
layout.assets ()
layout.bottom ()
layout.left ()
layout.right ()
layout.root ()
layout.toolbar ()
layout.toolbar.launch ()
layout.toolbar.scene ()
layout.viewport ()
lightmapper:auto (value)
lightmapper:bake (entities)
lightmapper:uv1missing (assets)
loadAsset (id, callback)
localStorage:get (key)
localStorage:has (key)
localStorage:set (key, value)
material:default (existingData)
material:rememberMissingFields (asset)
menu:entities:new (getParentFn)
menu:get (name)
notify (args)
notify:permission (fn)
notify:state ()
notify:title (title)
permissions ()
permissions:admin (userId)
permissions:read (userId)
permissions:write (userId)
picker:asset (type, asset)
picker:asset:close ()
picker:color (color)
picker:color:close ()
picker:color:position (x, y)
picker:color:rect ()
picker:color:set (color)
picker:confirm (text, fn)
picker:confirm:class (name)
picker:confirm:close ()
picker:curve (value, args)
picker:curve:close ()
picker:curve:position (x, y)
picker:curve:rect ()
picker:curve:set (value, args)
picker:entity (resourceId, fn)
picker:entity:close ()
picker:node (entities)
picker:node:close ()
picker:project (option)
picker:project:close ()
picker:project:registerMenu (name, title, panel)
picker:project:registerPanel (name, title, panel)
picker:project:setClosable (closable)
picker:project:setDefaultMenu (name)
picker:publish ()
picker:publish:download ()
picker:publish:new ()
picker:scene ()
picker:scene:close ()
picker:script-create (fn, string)
picker:script-create:close ()
picker:script-create:validate (filename)
preview:assetRegistry ()
preview:delayedRender (asset, delay)
preview:device ()
preview:loader ()
preview:prepare ()
preview:render (asset)
preview:render:cubemap (asset, size, callback)
preview:render:font (asset, size, callback)
preview:render:material (asset, size, callback)
preview:render:model (asset, size, callback)
preview:setThumbnail (asset, value)
project:save (data, success, error)
project:setPrimaryApp (appId, success, error)
project:setPrimaryScene (sceneId, success, error)
project:settings ()
realtime:assets:op (op, id)
realtime:connection ()
realtime:loadScene (id)
realtime:scene:op (op)
realtime:send (name, data)
realtime:subscribe:userdata (sceneId, userId)
realtime:userdata:op (op)
scene:load (id, isNew)
scene:unload ()
sceneSettings ()
scenes:delete (sceneId, callback)
scenes:duplicate (sceneId, newName, callback)
scenes:get (sceneId, callback)
scenes:list (callback)
scenes:new (name, callback)
scripts:parse (asset, fn)
search:charsContains (a, b)
search:items (items, search, args)
search:stringEditDistance (a, b)
search:stringTokenize (name)
selection:aabb ()
selector:add (type, item)
selector:clear (item)
selector:count ()
selector:enabled (state)
selector:has (item)
selector:history (toggle)
selector:items ()
selector:remove (item)
selector:set (type, items)
selector:toggle (type, item)
selector:type ()
sourcefiles:loadingScreen:skeleton ()
sourcefiles:skeleton (url)
status:error (text)
status:job (id, value)
status:text (text)
userdata ()
users:get (id)
users:isBetaTester (flag)
users:isSuperUser (flag)
users:loadOne (id, callback)
viewport:canvas ()
viewport:contextmenu (x, y, entity)
viewport:designer ()
viewport:expand (state)
viewport:expand:state ()
viewport:flyMode ()
viewport:focus ()
viewport:framework ()
viewport:icons:size (size)
viewport:inViewport ()
viewport:keepRendering (value)
viewport:pick (x, y, fn)
viewport:pick:filter (fn)
viewport:pick:state (state)
viewport:render ()
viewport:render:aabb (aabb)
visible ()
whoisonline:add (id)
whoisonline:color (id, type)
whoisonline:find (id)
whoisonline:get ()
whoisonline:panel ()
whoisonline:remove (id)
whoisonline:set (data)

And there are events as well, so you can subscribe to things when they happen, for example:

editor.on('assets:add', function(asset) {
    console.log(asset.get('name'), 'asset been added');
});

Here is useful method to get current selection type and list of objects:

var type = editor.call('selector:type');
var items = editor.call('selector:items');

To get JSON representation of selected item, I very often use this line:

console.log(editor.call('selector:items')[0].json());

Have a look into _hooks and editor.js to figure out stuff.

Please be aware this is not public functionality, and is a subject to change without any notification, so use fully on you own risk taking in account that changes to API might change and break behaviour of your logic, leading to nasty situations with data of your project.
Please bear in mind that many of this things might relate to our back-end, so make sure you are care and respectful to our resources, and avoid any spam of methods that lead to server load.


[SOLVED] Programmatically alter scene in Editor mode
Editor's gizmos inside an application
#3

Wow! That is simple amazing. Didn’t have a clue that the editor has such a kind of internal messaging system. Seems highly sophisticated.

I will keep in mind that the API is subject to change, but even for playing around all that info is treasure.

Thank you again @max,


#4

Hi max,

Thank you very much for sharing this. We are trying to craft a converter that would be able to re-assemble PlayCanvas scene in the editor from the other source (Unity at the moment).

Could you please elaborate on a few specific API calls?

  1. assets:create:*
    – how to supply arguments (i.e. JSON data for the asset)
    – how to specify binary content (if that’s an image for instance)

  2. entities:add
    – how to supply arguments (i.e. JSON data for the entity)

  3. entities:get
    – how to get scripts attached to an entity? I tried looking up in c property, as well as checking script property, but for the entities gathered through editor API these seem to be missing

Thank you very much in advance!


#5

Hi @Anton_Zhuravsky.

Best way, is to look into editor.js - which is not minified on purpose, and you can see examples of usage of those methods all round.
One good way to inspect this, it to set breakpoint in method you are calling, and then get it to be called from the Editor. For example search for editor.method('assets:create', function - this will point you to where this method is defined.

To provide binary data, you can check example of arguments in for example (search for) method('assets:create:css', function. And look at line with file: new Blob([ .. ], { type: '..' }), here is doc on Blob. It is the way to provide binary data as file.

For entities:add you need to provide new Observer object representing entity data.
Take a look at entities:new method in code - that creates new Observer object with provided data. Then you call entities:add to add that to project.

entities:get uses resource_id of an entity as ID. It will return Observer object.
With observer, you can then simply do: obj.json() - that will give you json representation of observer.
If you want to modify its data, use set method on it as described in second post in this topic.
If you inspect json of it, you will find scripts data in components.

Best way to get your head around it, is to create in Editor desirable entities, and then inspect their JSON representation to understand their structure.
And feel free to look into editor.js and search through it. I know it is concatenated all, which makes it a bit harder to look around, but it has everything inside :slight_smile:
Power of dev tools with breakpoints - will let you to inspect any goingons within editor :slight_smile:


#6

Is it possible to make the editor’s gizmos (rotate, translate, scale) appear in my application using the editor’s hooks?

Having read this thread, my understanding was that this was possible.
However, I have tried in my code this line:

editor.call(‘gizmo:translate:toggle’, true);

and it has failed.
Apparently gizmo:translate:toggle is not in Editor._hooks

Either the 333 item list is outdated, or I have totally missed the point of this thread…

Can someone guide me?
My only goal is to show the editor’s gizmos in my application.

Thanks.


#7

@max

So this might seem like a stupid question, but is it possible to write a script that can run in the editor and use the editor’s functionality when the parse button is pressed (the parse button does run the code, doesn’t it?)


#8

No that’s not possible. The only way to run Editor code at the moment is either though the browser console or by writing a browser extension.


#9

Thanks! Saved me a lot of time researching workers :slight_smile:


#10

Reviving the thread just to show how scriptable the PlayCanvas editor can be.

Using the Tiled Terrain Manager to generate a runtime terrain from heightmaps … I was amazed how simple it was to re-use the same codebase in the editor, with minor changes, and generate the exact same terrain for the level designers to use.

The open nature of Javascript combined with the awesomeness the PlayCanvas guys have built in the editor is powerful.


#11

Here is an example script that will position all of your selected entities in the X axis in a sequence.

  1. Select a number of entities in the editor.
  2. Open the browser console.
  3. Adjust the step in the first line, paste the code and execute.
translateChildrenToGrid(3);

function translateChildrenToGrid(step) {
  const type = editor.call("selector:type");

  if (type !== "entity") {
    return false;
  }

  const items = editor.call("selector:items");

  if (!items || items.length === 0) {
    return false;
  }

  items.forEach((item, index) => {
    editor.call("selector:set", "entity", [item]);
    item.set("position", [index * step, 0, 0]);
  });

  console.log("--- Finished execution ---");
}


#12

And another one, I found a missing action in editor: I am unable to assign a material on a Mesh Instance of multiple model assets at the same time. I would have to assign them one after the other.

  1. Select a number of Model assets.
  2. Open the browser console.
  3. Set your Material asset id and Mesh Instance slot, paste the code and execute.

assetModelSetMaterial(15325478, 0);

function assetModelSetMaterial(assetId, slot) {
  const type = editor.call("selector:type");

  if (type !== "asset") {
    return false;
  }

  const items = editor.call("selector:items");

  if (!items || items.length === 0) {
    return false;
  }

  items.forEach((item, index) => {
    item.set("data.mapping." + slot + ".material", assetId);
  });

  console.log("--- Finished execution ---");
}