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.

240 lines • 9.95 kB
import { isDevEnvironment } from "../debug/debug.js"; import { builtinComponentKeyName } from "../engine_constants.js"; import { debugExtension } from "../engine_default_parameters.js"; import { getLoader } from "../engine_gltf.js"; import { apply } from "../js-extensions/index.js"; import { maskGltfAssociation, resolveReferences } from "./extension_utils.js"; export const debug = debugExtension; const componentsArrayExportKey = "$___Export_Components"; export const EXTENSION_NAME = "NEEDLE_components"; class ExtensionData { [builtinComponentKeyName]; } class ExportData { node; nodeIndex; nodeDef; constructor(node, nodeIndex, nodeDef) { this.node = node; this.nodeIndex = nodeIndex; this.nodeDef = nodeDef; } } export class NEEDLE_components { get name() { return EXTENSION_NAME; } // #region export exportContext; objectToNodeMap = {}; context; writer; registerExport(exp) { //@ts-ignore exp.register(writer => { // we want to hook into BEFORE user data is written // because we want to remove the components list (circular references) // and replace them with the serialized data // the write node callback is called after user data is serialized // we could also traverse everything before export and remove components // but doing it like that we avoid traversing multiple times if ("serializeUserData" in writer) { //@ts-ignore const originalFunction = writer.serializeUserData.bind(writer); this.writer = writer; //@ts-ignore writer.serializeUserData = (o, def) => { try { const hadUserData = this.serializeUserData(o, def); if (hadUserData) //@ts-ignore writer.extensionsUsed[this.name] = true; originalFunction(o, def); } finally { this.afterSerializeUserData(o, def); } }; } return this; }); } beforeParse() { this.exportContext = {}; this.objectToNodeMap = {}; } // https://github.com/mrdoob/three.js/blob/efbfc67edc7f65cfcc61a389ffc5fd43ea702bc6/examples/jsm/exporters/GLTFExporter.js#L532 serializeUserData(node, _nodeDef) { const components = node.userData?.components; if (!components || components.length <= 0) return false; // delete components before serializing user data to avoid circular references delete node.userData.components; node[componentsArrayExportKey] = components; return true; } afterSerializeUserData(node, _nodeDef) { if (node.type === "Scene") { if (debug) console.log("DONE", JSON.stringify(_nodeDef)); } // reset userdata if (node[componentsArrayExportKey] === undefined) return; const components = node[componentsArrayExportKey]; delete node[componentsArrayExportKey]; if (components !== null) { node.userData.components = components; } // console.log(_nodeDef, _nodeDef.mesh); } writeNode(node, nodeDef) { const nodeIndex = this.writer.json.nodes.length; if (debug) console.log(node.name, nodeIndex, node.uuid); const context = new ExportData(node, nodeIndex, nodeDef); this.exportContext[nodeIndex] = context; this.objectToNodeMap[node.uuid] = nodeIndex; } ; afterParse(input) { if (debug) console.log("AFTER", input); for (const i in this.exportContext) { const context = this.exportContext[i]; const node = context.node; const nodeDef = context.nodeDef; const nodeIndex = context.nodeIndex; const components = node.userData?.components; if (!components || components.length <= 0) continue; // create data container const data = new ExtensionData(); nodeDef.extensions = nodeDef.extensions || {}; nodeDef.extensions[this.name] = data; this.context.object = node; this.context.nodeId = nodeIndex; this.context.objectToNode = this.objectToNodeMap; const serializedComponentData = []; for (const comp of components) { this.context.target = comp; const res = getLoader().writeBuiltinComponentData(comp, this.context); if (res !== null) { serializedComponentData.push(res); // (comp as unknown as ISerializationCallbackReceiver)?.onAfterSerialize?.call(comp); } } if (serializedComponentData.length > 0) { data[builtinComponentKeyName] = serializedComponentData; if (debug) console.log("DID WRITE", node, "nodeIndex", nodeIndex, serializedComponentData); } } } // ------------------------------------- // #region import parser; nodeToObjectMap = {}; /** The loaded gltf */ gltf = null; beforeRoot() { if (debug) console.log("BEGIN LOAD"); this.nodeToObjectMap = {}; return null; } async afterRoot(result) { this.gltf = result; const parser = result.parser; const ext = parser?.extensions; if (!ext) return; const hasExtension = ext[this.name]; if (debug) console.log("After root", result, this.parser, ext); const loadComponents = []; if (hasExtension === true) { const nodes = parser.json.nodes; if (nodes) { for (let i = 0; i < nodes.length; i++) { const obj = await parser.getDependency('node', i); this.nodeToObjectMap[i] = obj; } for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; const index = i; // node.mesh; const ext = node.extensions; if (!ext) continue; const data = ext[this.name]; if (!data) continue; if (debug) console.log("NODE", node); const obj = this.nodeToObjectMap[index]; if (!obj) { console.error("Could not find object for node index: " + index, node, parser); continue; } apply(obj); loadComponents.push(this.createComponents(result, node, obj, data)); } } } await Promise.all(loadComponents); for (const instance of parser.associations.keys()) { const value = parser.associations.get(instance); if (value?.materials != undefined) { const key = "/materials/" + value.materials; maskGltfAssociation(instance, key); } } } async createComponents(result, node, obj, data) { if (!data) return; const componentData = data[builtinComponentKeyName]; if (componentData) { const tasks = new Array(); if (debug) console.log(obj.name, componentData); for (const i in componentData) { const data = componentData[i]; if (debug) console.log("Serialized data", JSON.parse(JSON.stringify(data))); // Fix for https://linear.app/needle/issue/NE-6779/blender-export-has-missing-sharedmaterials if (data?.name === "MeshRenderer" || data?.name === "SkinnedMeshRenderer") { if (!data.sharedMaterials) { let success = false; if ("mesh" in node) { const meshIndex = node.mesh; if (typeof meshIndex === "number" && result.parser) { const meshDef = result.parser.json.meshes?.[meshIndex]; if (meshDef?.primitives) { data.sharedMaterials = meshDef.primitives.map(prim => { return "/materials/" + (prim.material ?? 0); }); success = true; } } } if (!success && (debug || isDevEnvironment())) { console.warn(`[NEEDLE_components] Component '${data.name}' on object '${obj.name}' is not added to a mesh or failed to retrieve materials from glTF.`); } } } if (data && this.parser) { tasks.push(resolveReferences(this.parser, data) .catch(e => console.error(`Error while resolving references (see console for details)\n`, e, obj, data))); } obj.userData = obj.userData || {}; obj.userData[builtinComponentKeyName] = obj.userData[builtinComponentKeyName] || []; obj.userData[builtinComponentKeyName].push(data); } await Promise.all(tasks).catch((e) => { console.error("Error while loading components", e); }); } } } //# sourceMappingURL=NEEDLE_components.js.map