UNPKG

@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.

194 lines (162 loc) • 6.54 kB
import { type Context, FrameEvent } from "./engine_context.js"; import { ContextEvent } from "./engine_context_registry.js"; export declare type Event = ContextEvent | FrameEvent; declare type LifecycleHookContext = { context: Context; } /** * A function that can be called during the Needle Engine frame event at a specific point * @link https://engine.needle.tools/docs/scripting.html#special-lifecycle-hooks */ export declare type LifecycleMethod = (this: LifecycleHookContext, ctx: Context) => void; /** * Options for `onStart(()=>{})` etc event hooks * @link https://engine.needle.tools/docs/scripting.html#special-lifecycle-hooks */ export declare type LifecycleMethodOptions = { /** * If true, the callback will only be called once */ once?: boolean }; declare type RegisteredLifecycleMethod = { method: LifecycleMethod, options: LifecycleMethodOptions }; const newMethods = new Map<Event, Array<RegisteredLifecycleMethod>>(); const allMethods = new Map<Event, Array<RegisteredLifecycleMethod>>(); let methodsWarningCounter = 0; /** register a function to be called during the Needle Engine frame event at a specific point * @param cb the function to call * @param evt the event to call the function at */ export function registerFrameEventCallback(cb: LifecycleMethod, evt: Event, opts?: LifecycleMethodOptions) { if (!newMethods.has(evt)) { newMethods.set(evt, new Array()); } newMethods.get(evt)!.push({ method: cb, options: { once: false, ...opts } }); // Warn if there are too many methods registered if (methodsWarningCounter < 30) { const existing = allMethods.get(evt); if (existing && existing?.length > 100) { methodsWarningCounter += 1; console.warn(`You have ${existing.length} methods registered for Event ${evt}. This might be a performance issue! Consider unregistering the methods when they are not needed anymore! To unregister you can call the function returned by your event hook (e.g.const unregister = onStart(...)) or by using the once option like onStart(()=>{}, { once:true }). See https://engine.needle.tools/docs/scripting.html#special-lifecycle-hooks for more information.`); } } } /** * unregister a function to be called during the Needle Engine frame event at a specific point */ export function unregisterFrameEventCallback(cb: LifecycleMethod, evt: Event) { const methods = allMethods.get(evt); if (methods) { for (let i = 0; i < methods.length; i++) { if (methods[i].method === cb) { methods.splice(i, 1); return; } } } const newMethodsArray = newMethods.get(evt); if (newMethodsArray) { for (let i = 0; i < newMethodsArray.length; i++) { if (newMethodsArray[i].method === cb) { newMethodsArray.splice(i, 1); return; } } } } export function invokeLifecycleFunctions(ctx: Context, evt: Event) { // When a context is created, we need to reset the started state // Because we want to e.g. invoke `onStart` again (even if it's the same context) if (evt === ContextEvent.ContextCreated) { _ignore.delete(ctx); } internalInvokeLifecycleFunctions(ctx, evt); } function internalInvokeLifecycleFunctions(ctx: Context, evt: Event) { // handle the initialized event like start. // This happens e.g. if onInitialized is registered AFTER the context has been created if (evt === FrameEvent.Start) { const initializeMethods = newMethods.get(ContextEvent.ContextCreated); if (initializeMethods) { internalInvokeLifecycleFunctions(ctx, ContextEvent.ContextCreated); } } const shouldBeInvokedOnce = evt === FrameEvent.Start || evt === ContextEvent.ContextCreated; const methods = allMethods.get(evt); if (methods) { if (methods.length > 0) { const array = methods; invoke(ctx, array, shouldBeInvokedOnce); } } const newMethodsArray = newMethods.get(evt); if (newMethodsArray) { if (newMethodsArray.length > 0) { // We copy the array here once because the array might be modified during the invoke // E.g. if onStart(() => onStart(() => {})) is called const array = [...newMethodsArray]; newMethodsArray.length = 0; invoke(ctx, array, shouldBeInvokedOnce); // if any of the new methods is still in the allMethods array, remove it if (array.length > 0) { if (!allMethods.has(evt)) { allMethods.set(evt, new Array()); } const methodsArray = allMethods.get(evt)!; methodsArray.push(...array); } } } } const bufferArray = new Array<RegisteredLifecycleMethod>(); const hookContext: LifecycleHookContext = { context: null as any as Context }; function invoke(ctx: Context, methods: Array<RegisteredLifecycleMethod>, invokeOnce: boolean) { bufferArray.length = 0; for (let i = 0; i < methods.length; i++) { bufferArray.push(methods[i]); } let ignoreSet = _ignore.get(ctx); for (let i = 0; i < bufferArray.length; i++) { const entry = bufferArray[i]; let invoke = true; if (ignoreSet && ignoreSet.has(entry)) { invoke = false; } if (invoke) { try { hookContext.context = ctx; entry.method?.call(hookContext, ctx); } catch (e) { console.error("Error in lifecycle method", e); } } // Remove the method if it's a one time call if (entry.options?.once) { for (let j = 0; j < methods.length; j++) { if (methods[j] === entry) { methods.splice(j, 1); break; } } } else if (invokeOnce) { if (!ignoreSet) { ignoreSet = new Set(); _ignore.set(ctx, ignoreSet); } ignoreSet!.add(entry); } } } const _ignore = new WeakMap<Context, Set<RegisteredLifecycleMethod>>();