@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.
233 lines • 9.27 kB
JavaScript
import { isDevEnvironment } from "../engine/debug/index.js";
const argumentsBuffer = new Array();
export class CallInfo {
/**
* When the CallInfo is enabled it will be invoked when the EventList is invoked
*/
enabled = true;
/**
* The target object to invoke the method on OR the function to invoke
*/
target;
methodName;
/**
* The arguments to invoke this method with
*/
arguments;
get canClone() {
return this.target instanceof Object;
}
constructor(target, methodName, args, enabled) {
this.target = target;
this.methodName = methodName || null;
this.arguments = args;
if (enabled != undefined)
this.enabled = enabled;
}
invoke(...args) {
if (this.enabled === false)
return;
// CallInfo can just contain a function
if (typeof this.target === "function") {
if (this.arguments) {
argumentsBuffer.length = 0;
// we pass the custom arguments first and then the event arguments (if any)
// this is so that invoke("myEvent") will take precedence over the event arguments
// see https://linear.app/needle/issue/NE-5507
if (args !== undefined && args.length > 0)
argumentsBuffer.push(...args);
argumentsBuffer.push(...this.arguments);
this.target(...this.arguments);
argumentsBuffer.length = 0;
}
else {
this.target(...args);
}
}
else if (this.methodName != null) {
const method = this.target[this.methodName];
// If the target is callable
if (typeof method === "function") {
if (this.arguments) {
argumentsBuffer.length = 0;
// we pass the custom arguments first and then the event arguments (if any)
// this is so that invoke("myEvent") will take precedence over the event arguments
// see https://linear.app/needle/issue/NE-5507
if (args !== undefined && args.length > 0)
argumentsBuffer.push(...args);
argumentsBuffer.push(...this.arguments);
method.call(this.target, ...argumentsBuffer);
argumentsBuffer.length = 0;
}
else {
method.call(this.target, ...args);
}
}
// If the target is a property
else {
if (this.arguments) {
this.target[this.methodName] = this.arguments[0] || args[0];
}
else {
this.target[this.methodName] = args[0];
}
}
}
}
}
const isUpperCase = (string) => /^[A-Z]*$/.test(string);
export class EventListEvent extends Event {
args;
}
/**
* The EventList is a class that can be used to create a list of event listeners that can be invoked
*/
export class EventList {
/** checked during instantiate to create a new instance */
isEventList = true;
/**
* @internal Used by the Needle Engine instantiate call to remap the event listeners to the new instance
*/
__internalOnInstantiate(ctx) {
const newMethods = new Array();
for (let i = 0; i < this.methods.length; i++) {
const method = this.methods[i];
if (method.target instanceof Function) {
// can not clone a function
}
else {
const target = method.target;
let key = target?.uuid;
if (target) {
key = target.guid;
}
if (key) {
const newTarget = ctx[key];
if (newTarget) {
// remap the arguments to the new instance (e.g. if an object is passed as an argument to the event list and this object has been cloned we want to remap it to the clone)
const newArguments = method.arguments?.map(arg => {
if (arg instanceof Object && arg.uuid) {
return ctx[arg.uuid];
}
else if (arg?.isComponent) {
return ctx[arg.guid];
}
return arg;
});
newMethods.push(new CallInfo(newTarget.clone, method.methodName, newArguments, method.enabled));
}
else if (isDevEnvironment()) {
console.warn("Could not find target for event listener");
}
}
}
}
const newInstance = new EventList(newMethods);
return newInstance;
}
target;
key;
// TODO: serialization should not take care of the args but instead give them to the eventlist directly
// so we can handle passing them on here instead of in the serializer
// this would also allow us to pass them on to the component EventTarget
/** set an event target to try invoke the EventTarget dispatchEvent when this EventList is invoked */
setEventTarget(key, target) {
this.key = key;
this.target = target;
if (this.key !== undefined) {
let temp = "";
let foundFirstLetter = false;
for (const c of this.key) {
if (foundFirstLetter && isUpperCase(c))
temp += "-";
foundFirstLetter = true;
temp += c.toLowerCase();
}
this.key = temp;
}
}
/** How many callback methods are subscribed to this event */
get listenerCount() { return this.methods?.length ?? 0; }
/** If the event is currently being invoked */
get isInvoking() { return this._isInvoking; }
_isInvoking = false;
// TODO: can we make functions serializable?
methods = [];
_methodsCopy = [];
static from(...evts) {
return new EventList(evts);
}
constructor(evts) {
this.methods = [];
if (Array.isArray(evts)) {
for (const evt of evts) {
if (evt instanceof CallInfo) {
this.methods.push(evt);
}
else if (typeof evt === "function") {
this.methods.push(new CallInfo(evt));
}
}
}
else {
if (typeof evts === "function") {
this.methods.push(new CallInfo(evts));
}
}
}
/** Invoke all the methods that are subscribed to this event */
invoke(...args) {
if (this._isInvoking) {
console.warn("Circular event invocation detected. Please check your event listeners for circular references.", this);
return false;
}
if (this.methods?.length <= 0)
return false;
this._isInvoking = true;
try {
// make a copy of the methods array to avoid issues when removing listeners during invocation
this._methodsCopy.length = 0;
this._methodsCopy.push(...this.methods);
// first invoke all the methods that were subscribed to this eventlist
for (const m of this._methodsCopy) {
m.invoke(...args);
}
// then try to dispatch the event on the object that is owning this eventlist
// with this we get automatic event listener support for unity events on all componnets
// so example for a component with a click UnityEvent you can also subscribe to the component like this:
// myComponent.addEventListener("click", args => {...")
if (typeof this.target === "object" && typeof this.key === "string") {
const fn = this.target["dispatchEvent"];
if (typeof fn === "function") {
const evt = new EventListEvent(this.key);
evt.args = args;
fn.call(this.target, evt);
}
}
}
finally {
this._isInvoking = false;
this._methodsCopy.length = 0;
}
return true;
}
/** Add a new event listener to this event */
addEventListener(cb) {
this.methods.push(new CallInfo(cb));
return cb;
}
removeEventListener(cb) {
if (!cb)
return;
for (let i = this.methods.length - 1; i >= 0; i--) {
if (this.methods[i].target === cb) {
this.methods[i].enabled = false;
this.methods.splice(i, 1);
}
}
}
removeAllEventListeners() {
this.methods.length = 0;
}
}
//# sourceMappingURL=EventList.js.map