@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.
200 lines (171 loc) • 7.39 kB
text/typescript
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn
import { getParam } from "./engine_utils.js";
// const _wrappedMethods = new WeakSet();
// export function wrap<T>(prototype: object, methodName: string, before: (t: T) => void, after: (t: T) => void) {
// const $key = Symbol(methodName + "-patched");
// const alreadyDefined = Object.getOwnPropertyDescriptor(prototype, methodName);
// if (alreadyDefined) {
// const originalRender = alreadyDefined.get;
// if (originalRender) {
// // Object.defineProperty(prototype, "render", {
// // set: function (this: any, value: Function) {
// // originalRender.call(this);
// // },
// // get: function (this: ) {
// // return originalRender.call(this);
// // }
// // });
// }
// }
// else {
// Object.defineProperty(prototype, methodName, {
// set: function (this: any, value: Function) {
// this[$key] = value;
// },
// get: function (this: any) {
// return this[$key];
// }
// });
// }
// }
// export declare type FieldPatch = (instance: object, oldValue: any, newValue: any) => any;
export type Prefix = (...args) => any;
export type Postfix = (...args) => any;
const debugPatch = getParam("debugpatch");
/**
* Use patcher for patching properties insteadof calling Object.defineProperty individually
* since this will cause conflicts if multiple patches need to be applied to the same property
*/
export function addPatch<T extends object>(prototype: T, fieldName: string, beforeCallback?: Prefix | null, afterCallback?: Postfix | null) {
const debug = debugPatch === fieldName;
// If no callbacks are provided, we don't need to do anything
if (!beforeCallback && !afterCallback) {
return;
}
// TODO: we probably want to turn this into a symbol to prevent anyone from overriding it
// But when we need to store the symbol per prototype to allow e.g. material disposing to iterate those and dispose all
// TODO: making symbols here breaks e.g. progressive textures because they iterate on the property keys
const backingField = fieldName + "___needle";// Symbol(fieldName);// + " (patched)";
internalAddPatch(prototype, fieldName, beforeCallback, afterCallback);
const desc = Object.getOwnPropertyDescriptor(prototype, fieldName);
const existing = prototype[fieldName];
if (debug) console.log("Patch", prototype.constructor.name, fieldName, desc, existing);
if (desc) {
if (debug) console.log("Apply patch with existing descriptor", prototype.constructor.name, fieldName, desc);
if (typeof desc.value === "function") {
prototype[fieldName] = ensureFunctionWrapped(desc.value, prototype, fieldName);
}
}
else {
if (debug) console.log("Create patch with new property", prototype.constructor.name, fieldName, desc);
Object.defineProperty(prototype, fieldName, {
set: function (this: object, value: any) {
if (typeof value === "function") {
// TODO: not sure if this is correct (if the value that is set is a function)
this[backingField] = ensureFunctionWrapped(value, prototype, fieldName);
}
else {
const prev = this[backingField];
executePrefixes(prototype, fieldName, this, prev, value);
this[backingField] = value;
executePostFixes(prototype, fieldName, this, prev, value);
}
},
get: function (this: any) {
const value = this[backingField];
if (typeof value === "function") {
if (value[backingField]) {
return value[backingField];
}
}
return value;
}
});
}
}
/** Removes prefix or postfix */
export function removePatch(prototype: object, fieldName: string, prefixOrPostfix: Prefix | Postfix) {
const patches = getPatches(prototype, fieldName);
if (patches) {
for (let i = patches.length - 1; i >= 0; i--) {
const patch = patches[i];
// Remove either the prefix or postfix
if (patch.prefix === prefixOrPostfix) {
patch.prefix = null;
}
if (patch.postfix === prefixOrPostfix) {
patch.postfix = null;
}
// If the patch is empty, remove it from the list
if (!patch.prefix && !patch.postfix) {
patches.splice(i, 1);
}
}
}
}
const $wrappedFunctionSymbol = Symbol("Needle:Patches:WrappedFunction");
function ensureFunctionWrapped(originalFunction: Function, prototype, fieldname) {
if (originalFunction[$wrappedFunctionSymbol]) {
return originalFunction;
}
const wrappedFunction = function (this: object, ...args: any[]) {
executePrefixes(prototype, fieldname, this, ...args);
const result = originalFunction.apply(this, args);
executePostFixes(prototype, fieldname, this, result, ...args);
return result;
}
wrappedFunction[$wrappedFunctionSymbol] = true;
return wrappedFunction;
}
export const NeedlePatchesKey = "Needle:Patches";
declare type PatchInfo = {
prefix?: Prefix | null;
postfix?: Postfix | null;
}
function patches(): WeakMap<object, Map<string, PatchInfo[]>> {
if (!globalThis[NeedlePatchesKey]) {
globalThis[NeedlePatchesKey] = new WeakMap<object, Map<string, PatchInfo[]>>();
}
return globalThis[NeedlePatchesKey];
}
function getPatches(prototype, fieldName: string) {
const patchesMap = patches().get(prototype);
if (!patchesMap) {
return null;
}
return patchesMap.get(fieldName);;
}
function internalAddPatch(prototype, fieldName: string, prefix?: Prefix | null, postfix?: Postfix | null) {
let patchesMap = patches().get(prototype);
if (!patchesMap) {
patchesMap = new Map();
patches().set(prototype, patchesMap);
}
let patchList = patchesMap.get(fieldName);
if (!patchList) {
patchList = [];
patchesMap.set(fieldName, patchList);
}
patchList.push({
prefix: prefix,
postfix: postfix
});
}
function executePrefixes(prototype, fieldName: string, instance: object, ...args) {
if (!instance) return;
const patches = getPatches(prototype, fieldName);
if (patches) {
for (const patchInfo of patches) {
patchInfo.prefix?.call(instance, ...args);
}
}
}
function executePostFixes(prototype, fieldName: string, instance: object, result: any, ...args) {
if (!instance) return;
const patches = getPatches(prototype, fieldName);
if (patches) {
for (const patchInfo of patches) {
patchInfo.postfix?.call(instance, result, ...args);
}
}
}