@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.
620 lines • 24.8 kB
JavaScript
import { $shadowDomOwner } from "../engine-components/ui/Symbols.js";
import { __internalNotifyObjectDestroyed as __internalRemoveReferences, disposeObjectResources } from "./engine_assetdatabase.js";
import { ComponentLifecycleEvents } from "./engine_components_internal.js";
import { activeInHierarchyFieldName } from "./engine_constants.js";
import { editorGuidKeyName } from "./engine_constants.js";
import { $isUsingInstancing, InstancingUtil } from "./engine_instancing.js";
import { processNewScripts } from "./engine_mainloop_utils.js";
import { InstantiateIdProvider } from "./engine_networking_instantiate.js";
import { assign } from "./engine_serialization_core.js";
import { Context, registerComponent } from "./engine_setup.js";
import { logHierarchy, setWorldPosition, setWorldQuaternion } from "./engine_three_utils.js";
import { getParam, tryFindObject } from "./engine_utils.js";
import { apply } from "./js-extensions/index.js";
const debug = getParam("debuggetcomponent");
const debugInstantiate = getParam("debuginstantiate");
/**
* Instantiation options for {@link syncInstantiate}
*/
export class InstantiateOptions {
idProvider;
parent;
keepWorldPosition;
position;
rotation;
scale;
visible;
context;
components;
clone() {
const clone = new InstantiateOptions();
clone.idProvider = this.idProvider;
clone.parent = this.parent;
clone.keepWorldPosition = this.keepWorldPosition;
clone.position = this.position?.clone();
clone.rotation = this.rotation?.clone();
clone.scale = this.scale?.clone();
clone.visible = this.visible;
clone.context = this.context;
clone.components = this.components;
return clone;
}
/** Copy fields from another object, clone field references */
cloneAssign(other) {
this.idProvider = other.idProvider;
this.parent = other.parent;
this.keepWorldPosition = other.keepWorldPosition;
this.position = other.position?.clone();
this.rotation = other.rotation?.clone();
this.scale = other.scale?.clone();
this.visible = other.visible;
this.context = other.context;
this.components = other.components;
}
}
// export function setActive(go: Object3D, active: boolean, processStart: boolean = true) {
// if (!go) return;
// go.visible = active;
// main.updateActiveInHierarchyWithoutEventCall(go);
// if (active && processStart)
// main.processStart(Context.Current, go);
// }
// Object.defineProperty(Object3D.prototype, "visible", {
// get: function () {
// return this._visible;
// },
// set: function (val) {
// // const changed = val !== this._visible;
// this._visible = val;
// // if (changed) {
// // setActive(this, val);
// // }
// }
// });
const $isActive = Symbol("isActive");
export function isActiveSelf(go) {
// if (go[$isActive] === undefined) {
// go[$isActive] = true;
// }
return go.visible;
}
export function setActive(go, active) {
if (typeof active === "number")
active = active > .5;
// go[$isActive] = active;
go.visible = active;
return go.visible; // go[$isActive];
}
export function isActiveInHierarchy(go) {
return go[activeInHierarchyFieldName] || isUsingInstancing(go);
}
export function markAsInstancedRendered(go, instanced) {
go[$isUsingInstancing] = instanced;
}
export function isUsingInstancing(instance) { return InstancingUtil.isUsingInstancing(instance); }
export function findByGuid(guid, hierarchy) {
return tryFindObject(guid, hierarchy, true, true);
}
const $isDestroyed = Symbol("isDestroyed");
export function isDestroyed(go) {
return go[$isDestroyed];
}
export function setDestroyed(go, value) {
go[$isDestroyed] = value;
}
const $isDontDestroy = Symbol("isDontDestroy");
/** Mark an Object3D or component as not destroyable
* @param instance the object to be marked as not destroyable
* @param value true if the object should not be destroyed in `destroy`
*/
export function setDontDestroy(instance, value = true) {
instance[$isDontDestroy] = value;
}
const destroyed_components = [];
const destroyed_objects = [];
export function destroy(instance, recursive = true, dispose = false) {
destroyed_components.length = 0;
destroyed_objects.length = 0;
internalDestroy(instance, recursive, true);
for (const comp of destroyed_components) {
comp.gameObject = null;
//@ts-ignore
comp.context = null;
}
// dipose resources and remove references
for (const obj of destroyed_objects) {
setDestroyed(obj, true);
if (dispose) {
disposeObjectResources(obj);
}
// This needs to be called after disposing because it removes the references to resources
__internalRemoveReferences(obj);
}
destroyed_objects.length = 0;
destroyed_components.length = 0;
}
function internalDestroy(instance, recursive = true, isRoot = true) {
if (instance === null || instance === undefined)
return;
const comp = instance;
if (comp.isComponent) {
// Handle Component
if (comp[$isDontDestroy])
return;
destroyed_components.push(comp);
const go = comp.gameObject;
comp.__internalDisable();
comp.__internalDestroy();
comp.gameObject = go;
return;
}
// handle Object3D
if (instance[$isDontDestroy])
return;
const obj = instance;
if (debug)
console.log(obj);
destroyed_objects.push(obj);
// first disable and call onDestroy on components
const components = obj.userData?.components;
if (components != null && Array.isArray(components)) {
let lastLength = components.length;
for (let i = 0; i < components.length; i++) {
const comp = components[i];
internalDestroy(comp, recursive, false);
// components will be removed from componentlist in destroy
if (components.length < lastLength) {
lastLength = components.length;
i--;
}
}
}
// then continue in children of the passed in object
if (recursive && obj.children) {
for (const ch of obj.children) {
internalDestroy(ch, recursive, false);
}
}
if (isRoot)
obj.removeFromParent();
}
export function foreachComponent(instance, cb, recursive = true) {
return internalForEachComponent(instance, cb, recursive);
}
export function* foreachComponentEnumerator(instance, type, includeChildren = false, maxLevel = 999, _currentLevel = 0) {
if (!instance?.userData.components)
return;
if (_currentLevel > maxLevel)
return;
for (const comp of instance.userData.components) {
if (type && comp?.isComponent === true && comp instanceof type) {
yield comp;
}
else {
yield comp;
}
}
if (includeChildren === true) {
for (const ch of instance.children) {
yield* foreachComponentEnumerator(ch, type, true, maxLevel, _currentLevel + 1);
}
}
}
function internalForEachComponent(instance, cb, recursive, level = 0) {
if (!instance)
return;
if (!instance.isObject3D) {
new Error("Expected Object3D but got " + instance);
}
if (level > 1000) {
console.warn("Failed to iterate components: too many levels");
return;
}
if (instance.userData?.components) {
for (let i = 0; i < instance.userData.components.length; i++) {
const comp = instance.userData.components[i];
if (comp?.isComponent === true) {
const res = cb(comp);
if (res !== undefined)
return res;
}
}
}
if (recursive && instance.children) {
// childArrayBuffer.length = 0;
// childArrayBuffer.push(...instance.children);
const nextLevel = level + 1;
for (let i = 0; i < instance.children.length; i++) {
const child = instance.children[i];
if (!child)
continue;
const res = internalForEachComponent(child, cb, recursive, nextLevel);
if (res !== undefined)
return res;
}
// childArrayBuffer.length = 0;
}
}
export function instantiate(instance, opts) {
if ("isAssetReference" in instance) {
return instance.instantiate(opts ?? undefined);
}
let options = null;
if (opts !== null && opts !== undefined) {
// if x is defined assume this is a vec3 - this is just to not break everything at once and stay a little bit backwards compatible
if (opts["x"] !== undefined) {
options = new InstantiateOptions();
options.position = opts;
}
else {
// if (opts instanceof InstantiateOptions)
options = opts;
}
}
let context = Context.Current;
if (options?.context)
context = options.context;
if (debug && context.alias)
console.log("context", context.alias);
// we need to create the id provider before calling internal instantiate because cloned gameobjects also create new guids
if (options && !options.idProvider) {
options.idProvider = new InstantiateIdProvider(Date.now());
}
const components = [];
const goMapping = {}; // used to resolve references on components to components on other gameobjects to their new counterpart
const skinnedMeshes = {}; // used to resolve skinned mesh bones
const clone = internalInstantiate(context, instance, options, components, goMapping, skinnedMeshes);
if (clone) {
resolveReferences(goMapping);
resolveAndBindSkinnedMeshBones(skinnedMeshes, goMapping);
}
if (debug) {
logHierarchy(instance, true);
logHierarchy(clone, true);
}
const guidsMap = {};
if (options?.components !== false) {
for (const i in components) {
const copy = components[i];
const oldGuid = copy.guid;
if (options && options.idProvider) {
copy.guid = options.idProvider.generateUUID();
guidsMap[oldGuid] = copy.guid;
if (debug)
console.log(copy.name, copy.guid);
}
registerComponent(copy, context);
if (copy.__internalNewInstanceCreated)
copy.__internalNewInstanceCreated();
}
for (const i in components) {
const copy = components[i];
if (copy.resolveGuids)
copy.resolveGuids(guidsMap);
if (copy.enabled === false)
continue;
else
copy.enabled = true;
}
processNewScripts(context);
}
return clone;
}
function internalInstantiate(context, instance, opts, componentsList, objectsMap, skinnedMeshesMap) {
if (!instance)
return null;
// Don't clone UI shadow objects
if (instance[$shadowDomOwner]) {
return null;
}
// prepare, remove things that dont work out of the box
// e.g. user data we want to manually clone
// also children throw errors (e.g. recursive toJson with nested meshes)
const userData = instance.userData;
instance.userData = {};
const children = instance.children;
instance.children = [];
const clone = instance.clone(false);
apply(clone);
// if(instance[$originalGuid])
// clone[$originalGuid] = instance[$originalGuid];
instance.userData = userData;
instance.children = children;
// make reference from old id to new object
objectsMap[instance.uuid] = { original: instance, clone: clone };
if (debugInstantiate)
console.log("ADD", instance, clone);
if (instance.type === "SkinnedMesh") {
skinnedMeshesMap[instance.uuid] = { original: instance, clone: clone };
}
// DO NOT EVER RENAME BECAUSE IT BREAKS / MIGHT BREAK ANIMATIONS
// clone.name += " (Clone)";
if (opts?.visible !== undefined)
clone.visible = opts.visible;
if (opts?.idProvider) {
clone.uuid = opts.idProvider.generateUUID();
const cloneGo = clone;
if (cloneGo)
cloneGo.guid = clone.uuid;
}
if (instance.animations && instance.animations.length > 0) {
clone.animations = [...instance.animations];
}
const parent = instance.parent;
if (parent) {
parent.add(clone);
}
// apply transform
if (opts?.position) {
setWorldPosition(clone, opts.position);
}
else
clone.position.copy(instance.position);
if (opts?.rotation) {
setWorldQuaternion(clone, opts.rotation);
}
else
clone.quaternion.copy(instance.quaternion);
if (opts?.scale) {
clone.scale.copy(opts.scale);
// TODO MAJOR: replace with worldscale
// clone.worldScale = opts.scale;
}
else
clone.scale.copy(instance.scale);
if (opts?.parent && opts.parent !== "scene") {
let requestedParent = null;
if (typeof opts.parent === "string") {
requestedParent = tryFindObject(opts.parent, context.scene, true);
}
else {
requestedParent = opts.parent;
}
if (requestedParent) {
const func = opts.keepWorldPosition === true ? requestedParent.attach : requestedParent.add;
if (!func)
console.error("Invalid parent object", requestedParent, "received when instantiating:", instance);
else
func.call(requestedParent, clone);
}
else
console.warn("could not find parent:", opts.parent);
}
for (const [key, value] of Object.entries(instance.userData)) {
if (key === "components")
continue;
clone.userData[key] = value;
}
if (instance.userData?.components) {
const components = instance.userData.components;
const newComponents = [];
clone.userData.components = newComponents;
for (let i = 0; i < components.length; i++) {
const comp = components[i];
const copy = new comp.constructor();
assign(copy, comp, undefined, {
// onAssign: (source, key, value) => {
// if (typeof value === "object") {
// const serializable = source as ISerializable;
// if (serializable?.$serializedTypes?.[key]) {
// console.debug("TODO CLONE", key, value);
// }
// }
// return value;
// }
});
// make sure the original guid stays intact
if (comp[editorGuidKeyName] !== undefined)
copy[editorGuidKeyName] = comp[editorGuidKeyName];
newComponents.push(copy);
copy.gameObject = clone;
// copy.transform = clone;
componentsList.push(copy);
objectsMap[comp.guid] = { original: comp, clone: copy };
ComponentLifecycleEvents.dispatchComponentLifecycleEvent("component-added", copy);
}
}
// children should just clone the original transform
if (opts) {
opts.position = undefined;
opts.rotation = undefined;
opts.scale = undefined;
opts.parent = undefined;
opts.visible = undefined;
}
for (const ch in instance.children) {
const child = instance.children[ch];
const newChild = internalInstantiate(context, child, opts, componentsList, objectsMap, skinnedMeshesMap);
if (newChild) {
objectsMap[newChild.uuid] = { original: child, clone: newChild };
clone.add(newChild);
}
}
return clone;
}
function resolveAndBindSkinnedMeshBones(skinnedMeshes, newObjectsMap) {
for (const key in skinnedMeshes) {
const val = skinnedMeshes[key];
const original = val.original;
const originalSkeleton = original.skeleton;
const clone = val.clone;
// clone.updateWorldMatrix(true, true);
if (!originalSkeleton) {
console.warn("Skinned mesh has no skeleton?", val);
continue;
}
const originalBones = originalSkeleton.bones;
const clonedSkeleton = clone.skeleton.clone();
clone.skeleton = clonedSkeleton;
clone.bindMatrix.clone().copy(original.bindMatrix);
// console.log(clone.bindMatrix)
clone.bindMatrixInverse.copy(original.bindMatrixInverse);
// clone.bindMatrix.multiplyScalar(.025);
// console.assert(originalSkeleton.uuid !== clonedSkeleton.uuid);
// console.assert(originalBones.length === clonedSkeleton.bones.length);
const bones = [];
clonedSkeleton.bones = bones;
for (let i = 0; i < originalBones.length; i++) {
const bone = originalBones[i];
const newBoneInfo = newObjectsMap[bone.uuid];
const clonedBone = newBoneInfo.clone;
// console.log("NEW BONE: ", clonedBone, "BEFORE", newBoneInfo.original);
bones.push(clonedBone);
}
// clone.skeleton = new Skeleton(bones);
// clone.skeleton.update();
// clone.pose();
// clone.scale.set(1,1,1);
// clone.position.y += .1;
// console.log("ORIG", original, "CLONE", clone);
}
for (const key in skinnedMeshes) {
const clone = skinnedMeshes[key].clone;
clone.skeleton.update();
// clone.skeleton.calculateInverses();
clone.bind(clone.skeleton, clone.bindMatrix);
clone.updateMatrixWorld(true);
// clone.pose();
}
}
// private static bindNewSkinnedMeshBones(source, clone) {
// const sourceLookup = new Map();
// const cloneLookup = new Map();
// // const clone = source.clone(false);
// function parallelTraverse(a, b, callback) {
// callback(a, b);
// for (let i = 0; i < a.children.length; i++) {
// parallelTraverse(a.children[i], b.children[i], callback);
// }
// }
// parallelTraverse(source, clone, function (sourceNode, clonedNode) {
// sourceLookup.set(clonedNode, sourceNode);
// cloneLookup.set(sourceNode, clonedNode);
// });
// clone.traverse(function (node) {
// if (!node.isSkinnedMesh) return;
// const clonedMesh = node;
// const sourceMesh = sourceLookup.get(node);
// const sourceBones = sourceMesh.skeleton.bones;
// clonedMesh.skeleton = sourceMesh.skeleton.clone();
// clonedMesh.bindMatrix.copy(sourceMesh.bindMatrix);
// clonedMesh.skeleton.bones = sourceBones.map(function (bone) {
// return cloneLookup.get(bone);
// });
// clonedMesh.bind(clonedMesh.skeleton, clonedMesh.bindMatrix);
// });
// return clone;
// }
function resolveReferences(newObjectsMap) {
// for every object that is newly created we want to update references to their newly created counterparts
// e.g. a collider instance referencing a rigidbody instance should be updated so that
// the cloned collider does not reference the cloned rigidbody (instead of the original rigidbody)
for (const key in newObjectsMap) {
const val = newObjectsMap[key];
const clone = val.clone;
// resolve references
if (clone?.isObject3D && clone?.userData?.components) {
for (let i = 0; i < clone.userData.components.length; i++) {
const copy = clone.userData.components[i];
// find referenced within a cloned gameobject
const entries = Object.entries(copy);
// console.log(copy, entries);
for (const [key, value] of entries) {
if (Array.isArray(value)) {
const clonedArray = [];
copy[key] = clonedArray;
// console.log(copy, key, value, copy[key]);
for (let i = 0; i < value.length; i++) {
const entry = value[i];
// push value types into new array
if (typeof entry !== "object") {
clonedArray.push(entry);
continue;
}
const res = postProcessNewInstance(copy, key, entry, newObjectsMap);
if (res !== undefined) {
if (debugInstantiate)
console.log("Found new instance for", key, entry, "->", res);
clonedArray.push(res);
}
else {
if (debugInstantiate)
console.warn("Could not find new instance for", key, entry);
clonedArray.push(entry);
}
}
// console.log(copy[key])
}
else if (typeof value === "object") {
const res = postProcessNewInstance(copy, key, value, newObjectsMap);
if (res !== undefined) {
copy[key] = res;
}
else {
if (debugInstantiate)
console.warn("Could not find new instance for", key, value);
}
}
}
}
}
}
}
function postProcessNewInstance(copy, key, value, newObjectsMap) {
if (value === null || value === undefined)
return;
if (value.isComponent === true) {
const originalGameObjectReference = value["gameObject"];
// console.log(key, value, originalGameObjectReference);
if (originalGameObjectReference) {
const id = originalGameObjectReference.uuid;
const newGameObject = newObjectsMap[id]?.clone;
if (!newGameObject) {
// reference has not changed!
if (debugInstantiate)
console.log("reference did not change", key, copy, value);
return;
}
const index = originalGameObjectReference.userData.components.indexOf(value);
if (index >= 0 && newGameObject.isObject3D) {
if (debugInstantiate)
console.log(key, id);
const found = newGameObject.userData.components[index];
return found;
}
else {
console.warn("could not find component", key, value);
}
}
}
else if (value.isObject3D === true) {
// console.log(value);
if (key === "gameObject")
return;
const originalGameObjectReference = value;
if (originalGameObjectReference) {
const id = originalGameObjectReference.uuid;
const newGameObject = newObjectsMap[id]?.clone;
if (newGameObject) {
if (debugInstantiate)
console.log(key, "old", value, "new", newGameObject);
return newGameObject;
}
}
}
else {
// create new instances for some types that we know should usually not be shared and can safely be cloned
if (value.isVector4 || value.isVector3 || value.isVector2 || value.isQuaternion || value.isEuler) {
return value.clone();
}
else if (value.isColor === true) {
return value.clone();
}
else if (value.isEventList === true) {
// create a new instance of the object
const copy = value.__internalOnInstantiate(newObjectsMap);
return copy;
}
}
}
//# sourceMappingURL=engine_gameobject.js.map