@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
138 lines • 5.82 kB
JavaScript
import { isDevEnvironment } from "./debug/index.js";
import { getComponent } from "./engine_components.js";
import { ContextRegistry } from "./engine_context_registry.js";
import { TypeStore } from "./engine_typestore.js";
/**
* Quick access to the current Needle Engine context from anywhere — no need to pass `ctx` around.
* Use it in React/Svelte/Vue components, button handlers, or plain JavaScript.
*
* Safe to import at module level, including in SSR environments.
* For pages with multiple `<needle-engine>` elements, use `ctx` directly instead.
*
* @experimental This API may change in future releases.
*
* @example
* import { needle } from "@needle-tools/engine";
* button.onclick = () => {
* needle.sceneData.Camera.OrbitControls.autoRotate = true;
* };
*/
export const needle = new Proxy({}, {
get(_target, prop) {
if (prop === "then")
return undefined; // not a Promise
const ctx = ContextRegistry.Current;
if (!ctx) {
const fn = isDevEnvironment() ? console.error : console.warn;
fn(`[needle] needle.${prop} was accessed before the scene started. Use "needle" inside event handlers or callbacks, not at module top-level. For setup code use: onStart(ctx => { ... })`);
return makeErrorProxy(`needle not ready — scene hasn't started yet`);
}
const val = ctx[prop];
return typeof val === "function" ? val.bind(ctx) : val;
},
set(_target, prop, value) {
const ctx = ContextRegistry.Current;
if (!ctx) {
const fn = isDevEnvironment() ? console.error : console.warn;
fn(`[needle] needle.${prop} was set before the scene started. Use "needle" inside event handlers or callbacks, not at module top-level. For setup code use: onStart(ctx => { ... })`);
return true;
}
ctx[prop] = value;
return true;
},
});
const cache = new WeakMap();
/**
* Returns a proxy that silently absorbs any property get/set and logs a
* developer-friendly warning. Used when a node or component lookup fails so
* that chained access like `ctx.sceneData.Foo.Bar.baz = 1` never throws.
*/
function makeErrorProxy(message) {
const handler = {
get(_t, prop) {
if (prop === "then")
return undefined; // not a Promise
const fn = isDevEnvironment() ? console.error : console.warn;
fn(`[SceneData] ${message}`);
return makeErrorProxy(message);
},
set(_t, prop) {
const fn = isDevEnvironment() ? console.error : console.warn;
fn(`[SceneData] ${message} (tried to set "${prop}")`);
return true; // suppress TypeError
},
};
return new Proxy({}, handler);
}
/**
* Returns a proxy for a scene node that exposes `$object`, `$components`,
* and child nodes as nested properties.
*/
function makeNodeProxy(ctx, node) {
return new Proxy({}, {
get(_t, prop) {
if (prop === "$object")
return node;
if (prop === "$components") {
return new Proxy({}, {
get(_t2, compName) {
if (compName === "then")
return undefined;
const ctor = TypeStore.get(compName);
if (!ctor)
return makeErrorProxy(`Component type "${compName}" not registered (node "${node.name}")`);
const comp = getComponent(node, ctor);
if (!comp)
return makeErrorProxy(`Component "${compName}" not found on node "${node.name}"`);
return comp;
}
});
}
if (prop === "then")
return undefined; // not a Promise
// Child node lookup by name
const child = node.children.find(c => c.name === prop) ?? null;
if (!child) {
const fn = isDevEnvironment() ? console.error : console.warn;
fn(`[SceneData] "${prop}" is not a child of "${node.name}". Use .$object to get the Three.js object or .$components.Name to access a component.`);
return makeErrorProxy(`"${prop}" not found on node "${node.name}"`);
}
return makeNodeProxy(ctx, child);
}
});
}
/**
* Returns a lazily-resolved proxy typed as {@link SceneData}.
* The proxy is cached per context — each context gets exactly one instance.
*
* Shape mirrors the generated `needle-bindings.gen.d.ts`:
* ctx.sceneData.MyGlb.Camera.$components.OrbitControls.autoRotate = true;
* ctx.sceneData.MyGlb.Camera.$object // → THREE.Camera
* ctx.sceneData.MyGlb.UI.Button.$components.Button // → Needle Button component
*
* GLB name is ignored at runtime (scene is already loaded).
* Node lookup starts at the scene root.
*/
export function getSceneData(ctx) {
let proxy = cache.get(ctx);
if (!proxy) {
proxy = new Proxy({}, {
get(_target, _glbName) {
// GLB name level — ignored at runtime, return node-name proxy
return new Proxy({}, {
get(_target, nodeName) {
if (nodeName === "then")
return undefined;
const node = ctx.scene.getObjectByName(nodeName) ?? null;
if (!node)
return makeErrorProxy(`Node "${nodeName}" not found in scene`);
return makeNodeProxy(ctx, node);
}
});
}
});
cache.set(ctx, proxy);
}
return proxy;
}
//# sourceMappingURL=engine_scenedata.js.map