ScenePluginAddon AGENTS.md
This document describes how to expose LumeScene interfaces to JavaScript/ArkTS via N-API bindings.
Module Dependencies
ScenePluginAddon (scene3d.node)
├── LumeScene::API — scene interfaces (IScene, INode, ICamera, etc.)
├── AGP3D::AGP3DAPI — Lume3D API
├── AGPRender::AGPRenderAPI — render API
├── LumeMeta::API — META property system API
├── EcsSerializer — ECS serialization
├── RuntimeUtil — runtime utilities
├── node-api-headers — N-API v8 (from npm)
└── [optional] PluginBoidsSwarmAPI — boids swarm plugin API
Architecture Overview
ScenePluginAddon is a N-API native addon (scene3d.node) that wraps LumeScene types:
TrueRootObject (native object storage, type tagging, lifetime)
|
+-- BaseObject (NAPI ctor, Finalize/Dispose lifecycle, property macros)
|
+-- SceneJS (Scene wrapper: static load(), factory methods, properties)
+-- NodeJS (BaseObject + NodeImpl: position/rotation/scale, children)
+-- CameraJS (BaseObject + NodeImpl: fov, near, far, projection)
+-- SpotLightJS (BaseObject + BaseLight: color, intensity, shadow)
+-- DirectionalLightJS
+-- PointLightJS
+-- SceneComponentJS (dynamic property auto-discovery)
Shared behavior via multiple inheritance: NodeImpl (shared node logic), BaseLight (shared light logic), SceneResourceImpl (shared resource logic).
Thread Safety
Thread-Safe (can be called from any thread)
| Class | Mechanism | Protected State |
|---|---|---|
NodeJSTaskQueue |
std::recursive_mutex + napi_threadsafe_function |
Task queue, scheduling, ref count |
ThreadSafeCallback (AnimationJS) |
napi_threadsafe_function |
Cross-thread JS callback invocation |
JSWrapperState (JsObjectCache) |
Atomic refcount + TSF dispatch | JS reference cleanup |
RenderResources (RenderContextJS) |
CORE_NS::Mutex |
Bitmap cache map |
NOT Thread-Safe (JS-thread only)
All JS wrapper classes and proxies must only be called from the JS thread. They have no internal synchronization and access NAPI objects.
| Class | Notes |
|---|---|
BaseObject, TrueRootObject |
Base wrappers. ExecSyncTask() dispatches to engine thread. |
SceneJS |
Has unused CORE_NS::Mutex — not actually thread-safe. |
NodeJS, CameraJS |
Delegate to NodeImpl, use ExecSyncTask. |
SpotLightJS, DirectionalLightJS, PointLightJS |
No synchronization. |
MeshJS, MaterialJS, BaseMaterial |
No synchronization. |
SceneComponentJS |
Dynamic property discovery, no locking. |
BloomConfiguration, PostProcJS, ImageJS |
Use ExecSyncTask for engine access. |
BoidsSwarmWorldJS |
Heavy ExecSyncTask usage for all ECS ops. |
AnimationJS (wrapper part) |
Uses ThreadSafeCallback for events, but wrapper is JS-thread only. |
Promise |
All methods documented "Call only from the JS thread." |
Vec3Proxy, Vec4Proxy, Vec2Proxy, QuatProxy, ColorProxy |
Comments: "should be executed in the javascript thread." |
PropertyProxy, ObjectPropertyProxy, TypeProxy |
JS-thread bound, no locking. |
DisposeContainer |
No synchronization. |
RenderContextJS |
Has unused CORE_NS::Mutex — not actually thread-safe. |
LumeScene Layer Thread Safety
When ScenePluginAddon calls into LumeScene, these rules apply:
- Thread-safe (call from any thread):
IScene,INode,ICamerapublic API — internally dispatches to ECS thread viaAddTaskOrRunDirectly(). - Thread-safe (internally locked):
RenderResource,Shader,Image,Mesh,RenderResourceManager— useshared_mutexormutex. - NOT thread-safe (engine/ECS thread only):
InternalScene,IEcs,IComponentManager,IEntityManager,EcsObjectinternals, all Component subclasses.
Public API Definition
The JS/ArkTS public API is defined in TypeScript declaration files:
- Primary:
@pr/LumeTS/ModuleDeclaration/api/graphics3d/scene.d.ts— Scene, SceneResourceFactory, SceneComponent, RenderContext, etc. - Related:
sceneNodes.d.ts(Camera, Light, Node, Geometry),sceneTypes.d.ts(Vec3, Color, enums),sceneResources.d.ts(Shader, Material, Animation, etc.)
APIs that appear in these .d.ts files are public. APIs exposed in NAPI but NOT in the .d.ts files are private/internal — they exist for internal use or testing and are not part of the public contract.
Step-by-Step: Exposing a New Type to JS
1. Register the class
File: src/register_module.cpp
void RegisterClasses(napi_env env, napi_value exports) {
MyTypeJS::Init(env, exports);
MyTypeJS::RegisterEnums(env, exports); // if it has enums
}
2. Create the JS wrapper class
Inherit from BaseObject (and NodeImpl for node types).
Required methods: Init(), constructor, GetInstanceImpl(), DisposeNative(), Finalize().
3. Define member properties
void MyTypeJS::Init(napi_env env, napi_value exports) {
BASE_NS::vector<napi_property_descriptor> props;
NodeImpl::GetPropertyDescs(props); // inherit base props
props.push_back(GetSetProperty<float, MyTypeJS, &MyTypeJS::GetSpeed, &MyTypeJS::SetSpeed>("speed"));
props.push_back(TROGetSetProperty<float, MyTypeJS, &MyTypeJS::GetSpeed, &MyTypeJS::SetSpeed>("speed"));
napi_value func;
napi_define_class(env, "MyType", NAPI_AUTO_LENGTH,
BaseObject::ctor<MyTypeJS>(), nullptr,
props.size(), props.data(), &func);
mis->StoreCtor("MyType", func);
}
| Template | Purpose |
|---|---|
GetProperty<Type, Class, &Getter>("name") |
Read-only |
GetSetProperty<Type, Class, &Getter, &Setter>("name") |
Read-write |
TROGetProperty<Type, Class, &F>("name") |
Via TrueRootObject dispatch |
TROGetSetProperty<Type, Class, &Getter, &Setter>("name") |
TRO read-write |
Method<FC, Class, &Method>("name") |
Instance method |
4. Proxy properties (Vec3, Quat, Color)
posProxy_ = BASE_NS::make_unique<Vec3Proxy>(env, node->Position());
Proxy hierarchy: PropertyProxy -> ObjectPropertyProxy -> Vec3Proxy/QuatProxy/ColorProxy
5. Dynamic properties (SceneComponentJS)
Auto-discovers properties via IComponent::EnumerateProperties(). Access: component.property.fieldName.
6. Accessing ECS-backed properties from JS
Use the full dotted property name for O(1) lookup:
BASE_NS::string fullName = compName + "." + jsKey;
auto prop = meta->GetProperty(fullName);
7. Enums
void MyTypeJS::RegisterEnums(napi_env env, napi_value exports) {
NapiApi::Object en(env);
napi_value v;
napi_create_uint32(env, static_cast<uint32_t>(MyEnum::VALUE_A), &v);
en.Set("VALUE_A", v);
exports.Set("MyEnum", en);
}
8. Lifetime management
PtrType::WEAK— Scene owns native object (nodes, cameras, lights)PtrType::STRONG— JS owns native object (materials, images, standalone resources)
9. Threading patterns
LumeScene public API (IScene, INode, ICamera, etc.) is thread-safe — these internally dispatch to the ECS thread via AddTaskOrRunDirectly(). However, InternalScene, raw ECS functions (IEcs, IComponentManager), and EcsObject internals are not thread-safe. The threading concern in ScenePluginAddon is about NAPI, not LumeScene public API.
- Async (Promise-based): Return
Promise, schedule resolution on JS thread viaNodeJSTaskQueue. - Sync from JS callbacks: Use
ExecSyncTask()when you need to perform engine work synchronously from a JS callback. This dispatches a task to the engine thread queue and blocks until complete.
Thread IDs:
- Engine:
2070e705-d061-40e4-bfb7-90fad2c280af - JS:
b2e8cef3-453a-4651-b564-5190f8b5190d
Cross-thread scheduling uses napi_threadsafe_function via NodeJSTaskQueue and JsObjectCache. The only NAPI function safe to call from non-JS threads is napi_call_threadsafe_function (via NodeJSTaskQueue::RescheduleTimer).
10. Native-to-JS class name mapping
If native and JS class names differ, add mapping in src/BaseObjectJS.cpp:GetConstructorName().
Key Files
| File | Role |
|---|---|
src/native_module_export.cpp |
NAPI module entry point |
src/register_module.cpp |
Central class registration hub |
include/BaseObjectJS.h |
Base class for all wrappers, ExecSyncTask |
src/BaseObjectJS.cpp |
CreateFromNativeInstance factory, name mapping |
include/TrueRootObject.h |
Native object storage, lifetime management |
include/napi/class_definition_helpers.h |
Property/method registration templates |
include/PropertyProxy.h |
Proxy hierarchy for complex types |
include/NodeImpl.h / src/NodeImpl.cpp |
Shared node logic |
src/SceneComponentJS.cpp |
Dynamic property auto-discovery |
src/nodejstaskqueue.cpp |
Thread-safe JS task scheduling |
src/JsObjectCache.cpp |
Thread-safe JS object cache |
Boundaries
Always
- Use the full dotted property name (
"ComponentName.propertyName") when accessing ECS-backed properties via the META system.
Ask First
- Which interfaces should be exposed as public API? Check the public API definition in
@pr/LumeTS/ModuleDeclaration/api/graphics3d/scene.d.ts(and related.d.tsfiles). New public APIs must be added to these declaration files. If the interface should NOT be public, explicitly confirm it is a private/internal API. - Are there private/internal NAPI APIs to add? Some APIs are exposed in NAPI but do not appear in the
.d.tsfiles (e.g., internal test interfaces, plugin-specific helpers). Confirm whether new bindings are public or private before implementing. - Which LumeScene interfaces need wrapping? Not all LumeScene interfaces have JS wrappers. Confirm which interfaces need exposure before creating new wrapper classes.
Never
- Do NOT call NAPI functions from a non-JS thread, with the sole exception of
napi_call_threadsafe_function(used internally byNodeJSTaskQueue). All othernapi_*functions — includingnapi_create_*,napi_get_reference_value,napi_call_function,napi_define_class,napi_create_object, etc. — MUST only be called on the JS thread. Violating this causes undefined behavior and crashes. - Do NOT call ECS or InternalScene functions directly from JS callbacks. Raw ECS functions (
IEcs,IComponentManager,IEntityManager),InternalScene, andEcsObjectinternals are not thread-safe and must be dispatched to the engine thread. The LumeScene public API (IScene,INode) is thread-safe. UseExecSyncTask()or async task scheduling for operations that need the engine thread.