UNPKG

@babylonjs/viewer

Version:

The Babylon Viewer aims to simplify a specific but common Babylon.js use case: loading, viewing, and interacting with a 3D model.

1,335 lines (1,325 loc) 141 kB
import { M as Matrix, a7 as Vector2, V as Vector3, i as Vector4, Q as Quaternion, aH as Color3, j as Color4, L as Logger, bm as RandomGUID, O as Observable, u as __decorate, v as serialize, R as RegisterClass, by as PointerEventTypes, bJ as unregisterGLTFExtension, bI as registerGLTFExtension } from './index-FzOfPXLV.esm.js'; import { F as FlowGraphMatrix2D, a as FlowGraphMatrix3D, b as FlowGraphInteger, g as getRichTypeByFlowGraphType, c as getMappingForDeclaration, d as getMappingForFullOperationName } from './declarationMapper-rcGCdcDP.esm.js'; import { a as GetPathToObjectConverter, A as AddObjectAccessorToKey } from './objectModelMapping-CJnQ6at_.esm.js'; function IsMeshClassName(className) { return (className === "Mesh" || className === "AbstractMesh" || className === "GroundMesh" || className === "InstanceMesh" || className === "LinesMesh" || className === "GoldbergMesh" || className === "GreasedLineMesh" || className === "TrailMesh"); } function IsVectorClassName(className) { return (className === "Vector2" /* FlowGraphTypes.Vector2 */ || className === "Vector3" /* FlowGraphTypes.Vector3 */ || className === "Vector4" /* FlowGraphTypes.Vector4 */ || className === "Quaternion" /* FlowGraphTypes.Quaternion */ || className === "Color3" /* FlowGraphTypes.Color3 */ || className === "Color4" /* FlowGraphTypes.Color4 */); } function IsMatrixClassName(className) { return className === "Matrix" /* FlowGraphTypes.Matrix */ || className === "Matrix2D" /* FlowGraphTypes.Matrix2D */ || className === "Matrix3D" /* FlowGraphTypes.Matrix3D */; } function IsAnimationGroupClassName(className) { return className === "AnimationGroup"; } function ParseVector(className, value, flipHandedness = false) { if (className === "Vector2" /* FlowGraphTypes.Vector2 */) { return Vector2.FromArray(value); } else if (className === "Vector3" /* FlowGraphTypes.Vector3 */) { if (flipHandedness) { value[2] *= -1; } return Vector3.FromArray(value); } else if (className === "Vector4" /* FlowGraphTypes.Vector4 */) { return Vector4.FromArray(value); } else if (className === "Quaternion" /* FlowGraphTypes.Quaternion */) { if (flipHandedness) { value[2] *= -1; value[3] *= -1; } return Quaternion.FromArray(value); } else if (className === "Color3" /* FlowGraphTypes.Color3 */) { return new Color3(value[0], value[1], value[2]); } else if (className === "Color4" /* FlowGraphTypes.Color4 */) { return new Color4(value[0], value[1], value[2], value[3]); } else { throw new Error(`Unknown vector class name ${className}`); } } /** * The default function that serializes values in a context object to a serialization object * @param key the key where the value should be stored in the serialization object * @param value the value to store * @param serializationObject the object where the value will be stored */ // eslint-disable-next-line @typescript-eslint/naming-convention function defaultValueSerializationFunction(key, value, serializationObject) { const className = value?.getClassName?.() ?? ""; if (IsVectorClassName(className) || IsMatrixClassName(className)) { serializationObject[key] = { value: value.asArray(), className, }; } else if (className === "FlowGraphInteger" /* FlowGraphTypes.Integer */) { serializationObject[key] = { value: value.value, className, }; } else { if (className && (value.id || value.name)) { serializationObject[key] = { id: value.id, name: value.name, className, }; } else { // only if it is not an object if (typeof value !== "object") { serializationObject[key] = value; } else { throw new Error(`Could not serialize value ${value}`); } } } } /** * The default function that parses values stored in a serialization object * @param key the key to the value that will be parsed * @param serializationObject the object that will be parsed * @param assetsContainer the assets container that will be used to find the objects * @param scene * @returns */ // eslint-disable-next-line @typescript-eslint/naming-convention function defaultValueParseFunction(key, serializationObject, assetsContainer, scene) { const intermediateValue = serializationObject[key]; let finalValue; const className = intermediateValue?.type ?? intermediateValue?.className; if (IsMeshClassName(className)) { let nodes = scene.meshes.filter((m) => (intermediateValue.id ? m.id === intermediateValue.id : m.name === intermediateValue.name)); if (nodes.length === 0) { nodes = scene.transformNodes.filter((m) => (intermediateValue.id ? m.id === intermediateValue.id : m.name === intermediateValue.name)); } finalValue = intermediateValue.uniqueId ? nodes.find((m) => m.uniqueId === intermediateValue.uniqueId) : nodes[0]; } else if (IsVectorClassName(className)) { finalValue = ParseVector(className, intermediateValue.value); } else if (IsAnimationGroupClassName(className)) { // do not use the scene.getAnimationGroupByName because it is possible that two AGs will have the same name const ags = scene.animationGroups.filter((ag) => ag.name === intermediateValue.name); // uniqueId changes on each load. this is used for the glTF loader, that uses serialization after the scene was loaded. finalValue = ags.length === 1 ? ags[0] : ags.find((ag) => ag.uniqueId === intermediateValue.uniqueId); } else if (className === "Matrix" /* FlowGraphTypes.Matrix */) { finalValue = Matrix.FromArray(intermediateValue.value); } else if (className === "Matrix2D" /* FlowGraphTypes.Matrix2D */) { finalValue = new FlowGraphMatrix2D(intermediateValue.value); } else if (className === "Matrix3D" /* FlowGraphTypes.Matrix3D */) { finalValue = new FlowGraphMatrix3D(intermediateValue.value); } else if (className === "FlowGraphInteger" /* FlowGraphTypes.Integer */) { finalValue = FlowGraphInteger.FromValue(intermediateValue.value); } else if (className === "number" /* FlowGraphTypes.Number */ || className === "string" /* FlowGraphTypes.String */ || className === "boolean" /* FlowGraphTypes.Boolean */) { finalValue = intermediateValue.value[0]; } else if (intermediateValue && intermediateValue.value !== undefined) { finalValue = intermediateValue.value; } else { if (Array.isArray(intermediateValue)) { // configuration data of an event finalValue = intermediateValue.reduce((acc, val) => { if (!val.eventData) { return acc; } acc[val.id] = { type: getRichTypeByFlowGraphType(val.type), }; if (typeof val.value !== "undefined") { acc[val.id].value = defaultValueParseFunction("value", val, assetsContainer, scene); } return acc; }, {}); } else { finalValue = intermediateValue; } } return finalValue; } /** * Given a name of a flow graph block class, return if this * class needs to be created with a path converter. Used in * parsing. * @param className the name of the flow graph block class * @returns a boolean indicating if the class needs a path converter */ // eslint-disable-next-line @typescript-eslint/naming-convention function needsPathConverter(className) { // I am not using the ClassName property here because it was causing a circular dependency // that jest didn't like! return className === "FlowGraphJsonPointerParserBlock" /* FlowGraphBlockNames.JsonPointerParser */; } /** * The type of the assets that flow graph supports */ var FlowGraphAssetType; (function (FlowGraphAssetType) { FlowGraphAssetType["Animation"] = "Animation"; FlowGraphAssetType["AnimationGroup"] = "AnimationGroup"; FlowGraphAssetType["Mesh"] = "Mesh"; FlowGraphAssetType["Material"] = "Material"; FlowGraphAssetType["Camera"] = "Camera"; FlowGraphAssetType["Light"] = "Light"; // Further asset types will be added here when needed. })(FlowGraphAssetType || (FlowGraphAssetType = {})); /** * Returns the asset with the given index and type from the assets context. * @param assetsContext The assets context to get the asset from * @param type The type of the asset * @param index The index of the asset * @param useIndexAsUniqueId If set to true, instead of the index in the array it will search for the unique id of the asset. * @returns The asset or null if not found */ function GetFlowGraphAssetWithType(assetsContext, type, index, useIndexAsUniqueId) { switch (type) { case "Animation" /* FlowGraphAssetType.Animation */: return useIndexAsUniqueId ? (assetsContext.animations.find((a) => a.uniqueId === index) ?? null) : (assetsContext.animations[index] ?? null); case "AnimationGroup" /* FlowGraphAssetType.AnimationGroup */: return useIndexAsUniqueId ? (assetsContext.animationGroups.find((a) => a.uniqueId === index) ?? null) : (assetsContext.animationGroups[index] ?? null); case "Mesh" /* FlowGraphAssetType.Mesh */: return useIndexAsUniqueId ? (assetsContext.meshes.find((a) => a.uniqueId === index) ?? null) : (assetsContext.meshes[index] ?? null); case "Material" /* FlowGraphAssetType.Material */: return useIndexAsUniqueId ? (assetsContext.materials.find((a) => a.uniqueId === index) ?? null) : (assetsContext.materials[index] ?? null); case "Camera" /* FlowGraphAssetType.Camera */: return useIndexAsUniqueId ? (assetsContext.cameras.find((a) => a.uniqueId === index) ?? null) : (assetsContext.cameras[index] ?? null); case "Light" /* FlowGraphAssetType.Light */: return useIndexAsUniqueId ? (assetsContext.lights.find((a) => a.uniqueId === index) ?? null) : (assetsContext.lights[index] ?? null); default: return null; } } var FlowGraphAction; (function (FlowGraphAction) { FlowGraphAction["ExecuteBlock"] = "ExecuteBlock"; FlowGraphAction["ExecuteEvent"] = "ExecuteEvent"; FlowGraphAction["TriggerConnection"] = "TriggerConnection"; FlowGraphAction["ContextVariableSet"] = "ContextVariableSet"; FlowGraphAction["GlobalVariableSet"] = "GlobalVariableSet"; FlowGraphAction["GlobalVariableDelete"] = "GlobalVariableDelete"; FlowGraphAction["GlobalVariableGet"] = "GlobalVariableGet"; FlowGraphAction["AddConnection"] = "AddConnection"; FlowGraphAction["GetConnectionValue"] = "GetConnectionValue"; FlowGraphAction["SetConnectionValue"] = "SetConnectionValue"; FlowGraphAction["ActivateSignal"] = "ActivateSignal"; FlowGraphAction["ContextVariableGet"] = "ContextVariableGet"; })(FlowGraphAction || (FlowGraphAction = {})); /** * This class will be responsible of logging the flow graph activity. * Note that using this class might reduce performance, as it will log every action, according to the configuration. * It attaches to a flow graph and uses meta-programming to replace the methods of the flow graph to add logging abilities. */ class FlowGraphLogger { constructor() { /** * Whether to log to the console. */ this.logToConsole = false; /** * The log cache of the flow graph. * Each item is a logged item, in order of execution. */ this.log = []; } addLogItem(item) { if (!item.time) { item.time = Date.now(); } this.log.push(item); if (this.logToConsole) { const value = item.payload?.value; if (typeof value === "object" && value.getClassName) { Logger.Log(`[FGLog] ${item.className}:${item.uniqueId.split("-")[0]} ${item.action} - ${JSON.stringify(value.getClassName())}: ${value.toString()}`); } else { Logger.Log(`[FGLog] ${item.className}:${item.uniqueId.split("-")[0]} ${item.action} - ${JSON.stringify(item.payload)}`); } } } getItemsOfType(action) { return this.log.filter((i) => i.action === action); } } /** * The context represents the current state and execution of the flow graph. * It contains both user-defined variables, which are derived from * a more general variable definition, and execution variables that * are set by the blocks. */ class FlowGraphContext { /** * Enable logging on this context */ get enableLogging() { return this._enableLogging; } set enableLogging(value) { if (this._enableLogging === value) { return; } this._enableLogging = value; if (this._enableLogging) { this.logger = new FlowGraphLogger(); this.logger.logToConsole = true; } else { this.logger = null; } } constructor(params) { /** * A randomly generated GUID for each context. */ this.uniqueId = RandomGUID(); /** * These are the variables defined by a user. */ this._userVariables = {}; /** * These are the variables set by the blocks. */ this._executionVariables = {}; /** * A context-specific global variables, available to all blocks in the context. */ this._globalContextVariables = {}; /** * These are the values for the data connection points */ this._connectionValues = {}; /** * These are blocks that have currently pending tasks/listeners that need to be cleaned up. */ this._pendingBlocks = []; /** * A monotonically increasing ID for each execution. * Incremented for every block executed. */ this._executionId = 0; /** * Observable that is triggered when a node is executed. */ this.onNodeExecutedObservable = new Observable(); /** * Whether to treat data as right-handed. * This is used when serializing data from a right-handed system, while running the context in a left-handed system, for example in glTF parsing. * Default is false. */ this.treatDataAsRightHanded = false; this._enableLogging = false; this._configuration = params; this.assetsContext = params.assetsContext ?? params.scene; } /** * Check if a user-defined variable is defined. * @param name the name of the variable * @returns true if the variable is defined */ hasVariable(name) { return name in this._userVariables; } /** * Set a user-defined variable. * @param name the name of the variable * @param value the value of the variable */ setVariable(name, value) { this._userVariables[name] = value; this.logger?.addLogItem({ time: Date.now(), className: this.getClassName(), uniqueId: this.uniqueId, action: "ContextVariableSet" /* FlowGraphAction.ContextVariableSet */, payload: { name, value, }, }); } /** * Get an assets from the assets context based on its type and index in the array * @param type The type of the asset * @param index The index of the asset * @returns The asset or null if not found */ getAsset(type, index) { return GetFlowGraphAssetWithType(this.assetsContext, type, index); } /** * Get a user-defined variable. * @param name the name of the variable * @returns the value of the variable */ getVariable(name) { this.logger?.addLogItem({ time: Date.now(), className: this.getClassName(), uniqueId: this.uniqueId, action: "ContextVariableGet" /* FlowGraphAction.ContextVariableGet */, payload: { name, value: this._userVariables[name], }, }); return this._userVariables[name]; } /** * Gets all user variables map */ get userVariables() { return this._userVariables; } /** * Get the scene that the context belongs to. * @returns the scene */ getScene() { return this._configuration.scene; } _getUniqueIdPrefixedName(obj, name) { return `${obj.uniqueId}_${name}`; } /** * @internal * @param name name of the variable * @param defaultValue default value to return if the variable is not defined * @returns the variable value or the default value if the variable is not defined */ _getGlobalContextVariable(name, defaultValue) { this.logger?.addLogItem({ time: Date.now(), className: this.getClassName(), uniqueId: this.uniqueId, action: "GlobalVariableGet" /* FlowGraphAction.GlobalVariableGet */, payload: { name, defaultValue, possibleValue: this._globalContextVariables[name], }, }); if (this._hasGlobalContextVariable(name)) { return this._globalContextVariables[name]; } else { return defaultValue; } } /** * Set a global context variable * @internal * @param name the name of the variable * @param value the value of the variable */ _setGlobalContextVariable(name, value) { this.logger?.addLogItem({ time: Date.now(), className: this.getClassName(), uniqueId: this.uniqueId, action: "GlobalVariableSet" /* FlowGraphAction.GlobalVariableSet */, payload: { name, value }, }); this._globalContextVariables[name] = value; } /** * Delete a global context variable * @internal * @param name the name of the variable */ _deleteGlobalContextVariable(name) { this.logger?.addLogItem({ time: Date.now(), className: this.getClassName(), uniqueId: this.uniqueId, action: "GlobalVariableDelete" /* FlowGraphAction.GlobalVariableDelete */, payload: { name }, }); delete this._globalContextVariables[name]; } /** * Check if a global context variable is defined * @internal * @param name the name of the variable * @returns true if the variable is defined */ _hasGlobalContextVariable(name) { return name in this._globalContextVariables; } /** * Set an internal execution variable * @internal * @param name * @param value */ _setExecutionVariable(block, name, value) { this._executionVariables[this._getUniqueIdPrefixedName(block, name)] = value; } /** * Get an internal execution variable * @internal * @param name * @returns */ _getExecutionVariable(block, name, defaultValue) { if (this._hasExecutionVariable(block, name)) { return this._executionVariables[this._getUniqueIdPrefixedName(block, name)]; } else { return defaultValue; } } /** * Delete an internal execution variable * @internal * @param block * @param name */ _deleteExecutionVariable(block, name) { delete this._executionVariables[this._getUniqueIdPrefixedName(block, name)]; } /** * Check if an internal execution variable is defined * @internal * @param block * @param name * @returns */ _hasExecutionVariable(block, name) { return this._getUniqueIdPrefixedName(block, name) in this._executionVariables; } /** * Check if a connection value is defined * @internal * @param connectionPoint * @returns */ _hasConnectionValue(connectionPoint) { return connectionPoint.uniqueId in this._connectionValues; } /** * Set a connection value * @internal * @param connectionPoint * @param value */ _setConnectionValue(connectionPoint, value) { this._connectionValues[connectionPoint.uniqueId] = value; this.logger?.addLogItem({ time: Date.now(), className: this.getClassName(), uniqueId: this.uniqueId, action: "SetConnectionValue" /* FlowGraphAction.SetConnectionValue */, payload: { connectionPointId: connectionPoint.uniqueId, value, }, }); } /** * Set a connection value by key * @internal * @param key the key of the connection value * @param value the value of the connection */ _setConnectionValueByKey(key, value) { this._connectionValues[key] = value; } /** * Get a connection value * @internal * @param connectionPoint * @returns */ _getConnectionValue(connectionPoint) { this.logger?.addLogItem({ time: Date.now(), className: this.getClassName(), uniqueId: this.uniqueId, action: "GetConnectionValue" /* FlowGraphAction.GetConnectionValue */, payload: { connectionPointId: connectionPoint.uniqueId, value: this._connectionValues[connectionPoint.uniqueId], }, }); return this._connectionValues[connectionPoint.uniqueId]; } /** * Get the configuration * @internal * @param name * @param value */ get configuration() { return this._configuration; } /** * Check if there are any pending blocks in this context * @returns true if there are pending blocks */ get hasPendingBlocks() { return this._pendingBlocks.length > 0; } /** * Add a block to the list of blocks that have pending tasks. * @internal * @param block */ _addPendingBlock(block) { // check if block is already in the array if (this._pendingBlocks.includes(block)) { return; } this._pendingBlocks.push(block); // sort pending blocks by priority this._pendingBlocks.sort((a, b) => a.priority - b.priority); } /** * Remove a block from the list of blocks that have pending tasks. * @internal * @param block */ _removePendingBlock(block) { const index = this._pendingBlocks.indexOf(block); if (index !== -1) { this._pendingBlocks.splice(index, 1); } } /** * Clear all pending blocks. * @internal */ _clearPendingBlocks() { for (const block of this._pendingBlocks) { block._cancelPendingTasks(this); } this._pendingBlocks.length = 0; } /** * @internal * Function that notifies the node executed observable * @param node */ _notifyExecuteNode(node) { this.onNodeExecutedObservable.notifyObservers(node); this.logger?.addLogItem({ time: Date.now(), className: node.getClassName(), uniqueId: node.uniqueId, action: "ExecuteBlock" /* FlowGraphAction.ExecuteBlock */, }); } _notifyOnTick(framePayload) { // set the values as global variables this._setGlobalContextVariable("timeSinceStart", framePayload.timeSinceStart); this._setGlobalContextVariable("deltaTime", framePayload.deltaTime); // iterate the pending blocks and run each one's onFrame function for (const block of this._pendingBlocks) { block._executeOnTick?.(this); } } /** * @internal */ _increaseExecutionId() { this._executionId++; } /** * A monotonically increasing ID for each execution. * Incremented for every block executed. */ get executionId() { return this._executionId; } /** * Serializes a context * @param serializationObject the object to write the values in * @param valueSerializationFunction a function to serialize complex values */ serialize(serializationObject = {}, valueSerializationFunction = defaultValueSerializationFunction) { serializationObject.uniqueId = this.uniqueId; serializationObject._userVariables = {}; for (const key in this._userVariables) { valueSerializationFunction(key, this._userVariables[key], serializationObject._userVariables); } serializationObject._connectionValues = {}; for (const key in this._connectionValues) { valueSerializationFunction(key, this._connectionValues[key], serializationObject._connectionValues); } // serialize assets context, if not scene if (this.assetsContext !== this.getScene()) { serializationObject._assetsContext = { meshes: this.assetsContext.meshes.map((m) => m.id), materials: this.assetsContext.materials.map((m) => m.id), textures: this.assetsContext.textures.map((m) => m.name), animations: this.assetsContext.animations.map((m) => m.name), lights: this.assetsContext.lights.map((m) => m.id), cameras: this.assetsContext.cameras.map((m) => m.id), sounds: this.assetsContext.sounds?.map((m) => m.name), skeletons: this.assetsContext.skeletons.map((m) => m.id), particleSystems: this.assetsContext.particleSystems.map((m) => m.name), geometries: this.assetsContext.geometries.map((m) => m.id), multiMaterials: this.assetsContext.multiMaterials.map((m) => m.id), transformNodes: this.assetsContext.transformNodes.map((m) => m.id), }; } } /** * @returns the class name of the object. */ getClassName() { return "FlowGraphContext"; } } __decorate([ serialize() ], FlowGraphContext.prototype, "uniqueId", void 0); /** * The type of a connection point - input or output. */ var FlowGraphConnectionType; (function (FlowGraphConnectionType) { FlowGraphConnectionType[FlowGraphConnectionType["Input"] = 0] = "Input"; FlowGraphConnectionType[FlowGraphConnectionType["Output"] = 1] = "Output"; })(FlowGraphConnectionType || (FlowGraphConnectionType = {})); /** * The base connection class. */ class FlowGraphConnection { constructor(name, _connectionType, /* @internal */ _ownerBlock) { this._ownerBlock = _ownerBlock; /** @internal */ this._connectedPoint = []; /** * A uniquely identifying string for the connection. */ this.uniqueId = RandomGUID(); /** * Used for parsing connections. * @internal */ // disable warning as this is used for parsing // eslint-disable-next-line @typescript-eslint/no-unused-vars this.connectedPointIds = []; this.name = name; this._connectionType = _connectionType; } /** * The type of the connection */ get connectionType() { return this._connectionType; } /** * @internal * Override this to indicate if a point can connect to more than one point. */ _isSingularConnection() { return true; } /** * Returns if a point is connected to any other point. * @returns boolean indicating if the point is connected. */ isConnected() { return this._connectedPoint.length > 0; } /** * Connects two connections together. * @param point the connection to connect to. */ connectTo(point) { if (this._connectionType === point._connectionType) { throw new Error(`Cannot connect two points of type ${this.connectionType}`); } if ((this._isSingularConnection() && this._connectedPoint.length > 0) || (point._isSingularConnection() && point._connectedPoint.length > 0)) { throw new Error("Max number of connections for point reached"); } this._connectedPoint.push(point); point._connectedPoint.push(this); } /** * Disconnects two connections. * @param point the connection to disconnect from. * @param removeFromLocal if true, the connection will be removed from the local connection list. */ disconnectFrom(point, removeFromLocal = true) { const indexLocal = this._connectedPoint.indexOf(point); const indexConnected = point._connectedPoint.indexOf(this); if (indexLocal === -1 || indexConnected === -1) { return; } if (removeFromLocal) { this._connectedPoint.splice(indexLocal, 1); } point._connectedPoint.splice(indexConnected, 1); } /** * Disconnects all connected points. */ disconnectFromAll() { for (const point of this._connectedPoint) { this.disconnectFrom(point, false); } this._connectedPoint.length = 0; } dispose() { for (const point of this._connectedPoint) { this.disconnectFrom(point); } } /** * Saves the connection to a JSON object. * @param serializationObject the object to serialize to. */ serialize(serializationObject = {}) { serializationObject.uniqueId = this.uniqueId; serializationObject.name = this.name; serializationObject._connectionType = this._connectionType; serializationObject.connectedPointIds = []; serializationObject.className = this.getClassName(); for (const point of this._connectedPoint) { serializationObject.connectedPointIds.push(point.uniqueId); } } /** * @returns class name of the connection. */ getClassName() { return "FGConnection"; } /** * Deserialize from a object into this * @param serializationObject the object to deserialize from. */ deserialize(serializationObject) { this.uniqueId = serializationObject.uniqueId; this.name = serializationObject.name; this._connectionType = serializationObject._connectionType; this.connectedPointIds = serializationObject.connectedPointIds; } } /** * Represents a connection point for data. * An unconnected input point can have a default value. * An output point will only have a value if it is connected to an input point. Furthermore, * if the point belongs to a "function" node, the node will run its function to update the value. */ class FlowGraphDataConnection extends FlowGraphConnection { /** * Create a new data connection point. * @param name the name of the connection * @param connectionType the type of the connection * @param ownerBlock the block that owns this connection * @param richType the type of the data in this block * @param _defaultValue the default value of the connection * @param _optional if the connection is optional */ constructor(name, connectionType, ownerBlock, /** * the type of the data in this block */ richType, /** * [any] the default value of the connection */ _defaultValue = richType.defaultValue, /** * [false] if the connection is optional */ _optional = false) { super(name, connectionType, ownerBlock); this.richType = richType; this._defaultValue = _defaultValue; this._optional = _optional; this._isDisabled = false; /** * This is used for debugging purposes! It is the last value that was set to this connection with ANY context. * Do not use this value for anything else, as it might be wrong if used in a different context. */ this._lastValue = null; /** * a data transformer function, if needed. * This can be used, for example, to force seconds into milliseconds output, if it makes sense in your case. */ this.dataTransformer = null; /** * An observable that is triggered when the value of the connection changes. */ this.onValueChangedObservable = new Observable(); } /** * Whether or not the connection is optional. * Currently only used for UI control. */ get optional() { return this._optional; } /** * is this connection disabled * If the connection is disabled you will not be able to connect anything to it. */ get isDisabled() { return this._isDisabled; } set isDisabled(value) { if (this._isDisabled === value) { return; } this._isDisabled = value; if (this._isDisabled) { this.disconnectFromAll(); } } /** * An output data block can connect to multiple input data blocks, * but an input data block can only connect to one output data block. * @returns true if the connection is singular */ _isSingularConnection() { return this.connectionType === 0 /* FlowGraphConnectionType.Input */; } /** * Set the value of the connection in a specific context. * @param value the value to set * @param context the context to which the value is set */ setValue(value, context) { // check if the value is different if (context._getConnectionValue(this) === value) { return; } context._setConnectionValue(this, value); this.onValueChangedObservable.notifyObservers(value); } /** * Reset the value of the connection to the default value. * @param context the context in which the value is reset */ resetToDefaultValue(context) { context._setConnectionValue(this, this._defaultValue); } /** * Connect this point to another point. * @param point the point to connect to. */ connectTo(point) { if (this._isDisabled) { return; } super.connectTo(point); } _getValueOrDefault(context) { const val = context._getConnectionValue(this) ?? this._defaultValue; return this.dataTransformer ? this.dataTransformer(val) : val; } /** * Gets the value of the connection in a specific context. * @param context the context from which the value is retrieved * @returns the value of the connection */ getValue(context) { if (this.connectionType === 1 /* FlowGraphConnectionType.Output */) { context._notifyExecuteNode(this._ownerBlock); this._ownerBlock._updateOutputs(context); const value = this._getValueOrDefault(context); this._lastValue = value; return this.richType.typeTransformer ? this.richType.typeTransformer(value) : value; } const value = !this.isConnected() ? this._getValueOrDefault(context) : this._connectedPoint[0].getValue(context); this._lastValue = value; return this.richType.typeTransformer ? this.richType.typeTransformer(value) : value; } /** * @internal */ _getLastValue() { return this._lastValue; } /** * @returns class name of the object. */ getClassName() { return "FlowGraphDataConnection"; } /** * Serializes this object. * @param serializationObject the object to serialize to */ serialize(serializationObject = {}) { super.serialize(serializationObject); serializationObject.richType = {}; this.richType.serialize(serializationObject.richType); serializationObject.optional = this._optional; defaultValueSerializationFunction("defaultValue", this._defaultValue, serializationObject); } } RegisterClass("FlowGraphDataConnection", FlowGraphDataConnection); /** * A block in a flow graph. The most basic form * of a block has inputs and outputs that contain * data. */ class FlowGraphBlock { /** Constructor is protected so only subclasses can be instantiated * @param config optional configuration for this block * @internal - do not use directly. Extend this class instead. */ constructor( /** * the configuration of the block */ config) { this.config = config; /** * A randomly generated GUID for each block. */ this.uniqueId = RandomGUID(); this.name = this.config?.name ?? this.getClassName(); this.dataInputs = []; this.dataOutputs = []; } /** * @internal * This function is called when the block needs to update its output flows. * @param _context the context in which it is running */ _updateOutputs(_context) { // empty by default, overridden in data blocks } /** * Registers a data input on the block. * @param name the name of the input * @param richType the type of the input * @param defaultValue optional default value of the input. If not set, the rich type's default value will be used. * @returns the created connection */ registerDataInput(name, richType, defaultValue) { const input = new FlowGraphDataConnection(name, 0 /* FlowGraphConnectionType.Input */, this, richType, defaultValue); this.dataInputs.push(input); return input; } /** * Registers a data output on the block. * @param name the name of the input * @param richType the type of the input * @param defaultValue optional default value of the input. If not set, the rich type's default value will be used. * @returns the created connection */ registerDataOutput(name, richType, defaultValue) { const output = new FlowGraphDataConnection(name, 1 /* FlowGraphConnectionType.Output */, this, richType, defaultValue); this.dataOutputs.push(output); return output; } /** * Given the name of a data input, returns the connection if it exists * @param name the name of the input * @returns the connection if it exists, undefined otherwise */ getDataInput(name) { return this.dataInputs.find((i) => i.name === name); } /** * Given the name of a data output, returns the connection if it exists * @param name the name of the output * @returns the connection if it exists, undefined otherwise */ getDataOutput(name) { return this.dataOutputs.find((i) => i.name === name); } /** * Serializes this block * @param serializationObject the object to serialize to * @param _valueSerializeFunction a function that serializes a specific value */ serialize(serializationObject = {}, _valueSerializeFunction = defaultValueSerializationFunction) { serializationObject.uniqueId = this.uniqueId; serializationObject.config = {}; if (this.config) { const config = this.config; const keys = Object.keys(config); for (const key of keys) { _valueSerializeFunction(key, config[key], serializationObject.config); } } serializationObject.dataInputs = []; serializationObject.dataOutputs = []; serializationObject.className = this.getClassName(); for (const input of this.dataInputs) { const serializedInput = {}; input.serialize(serializedInput); serializationObject.dataInputs.push(serializedInput); } for (const output of this.dataOutputs) { const serializedOutput = {}; output.serialize(serializedOutput); serializationObject.dataOutputs.push(serializedOutput); } } /** * Deserializes this block * @param _serializationObject the object to deserialize from */ deserialize(_serializationObject) { // no-op by default } _log(context, action, payload) { context.logger?.addLogItem({ action, payload, className: this.getClassName(), uniqueId: this.uniqueId, }); } /** * Gets the class name of this block * @returns the class name */ getClassName() { return "FlowGraphBlock"; } } /** * Represents a connection point for a signal. * When an output point is activated, it will activate the connected input point. * When an input point is activated, it will execute the block it belongs to. */ class FlowGraphSignalConnection extends FlowGraphConnection { constructor() { super(...arguments); /** * The priority of the signal. Signals with higher priority will be executed first. * Set priority before adding the connection as sorting happens only when the connection is added. */ this.priority = 0; } _isSingularConnection() { return false; } connectTo(point) { super.connectTo(point); // sort according to priority to handle execution order this._connectedPoint.sort((a, b) => b.priority - a.priority); } /** * @internal */ _activateSignal(context) { context.logger?.addLogItem({ action: "ActivateSignal" /* FlowGraphAction.ActivateSignal */, className: this._ownerBlock.getClassName(), uniqueId: this._ownerBlock.uniqueId, payload: { connectionType: this.connectionType, name: this.name, }, }); if (this.connectionType === 0 /* FlowGraphConnectionType.Input */) { context._notifyExecuteNode(this._ownerBlock); this._ownerBlock._execute(context, this); context._increaseExecutionId(); } else { for (const connectedPoint of this._connectedPoint) { connectedPoint._activateSignal(context); } } } } RegisterClass("FlowGraphSignalConnection", FlowGraphSignalConnection); /** * A block that executes some action. Always has an input signal (which is not used by event blocks). * Can have one or more output signals. */ class FlowGraphExecutionBlock extends FlowGraphBlock { constructor(config) { super(config); /** * The priority of the block. Higher priority blocks will be executed first. * Note that priority cannot be change AFTER the block was added as sorting happens when the block is added to the execution queue. */ this.priority = 0; this.signalInputs = []; this.signalOutputs = []; this.in = this._registerSignalInput("in"); this.error = this._registerSignalOutput("error"); } _registerSignalInput(name) { const input = new FlowGraphSignalConnection(name, 0 /* FlowGraphConnectionType.Input */, this); this.signalInputs.push(input); return input; } _registerSignalOutput(name) { const output = new FlowGraphSignalConnection(name, 1 /* FlowGraphConnectionType.Output */, this); this.signalOutputs.push(output); return output; } _unregisterSignalInput(name) { const index = this.signalInputs.findIndex((input) => input.name === name); if (index !== -1) { this.signalInputs[index].dispose(); this.signalInputs.splice(index, 1); } } _unregisterSignalOutput(name) { const index = this.signalOutputs.findIndex((output) => output.name === name); if (index !== -1) { this.signalOutputs[index].dispose(); this.signalOutputs.splice(index, 1); } } _reportError(context, error) { this.error.payload = typeof error === "string" ? new Error(error) : error; this.error._activateSignal(context); } /** * Given a name of a signal input, return that input if it exists * @param name the name of the input * @returns if the input exists, the input. Otherwise, undefined. */ getSignalInput(name) { return this.signalInputs.find((input) => input.name === name); } /** * Given a name of a signal output, return that input if it exists * @param name the name of the input * @returns if the input exists, the input. Otherwise, undefined. */ getSignalOutput(name) { return this.signalOutputs.find((output) => output.name === name); } /** * Serializes this block * @param serializationObject the object to serialize in */ serialize(serializationObject = {}) { super.serialize(serializationObject); serializationObject.signalInputs = []; serializationObject.signalOutputs = []; for (const input of this.signalInputs) { const serializedInput = {}; input.serialize(serializedInput); serializationObject.signalInputs.push(serializedInput); } for (const output of this.signalOutputs) { const serializedOutput = {}; output.serialize(serializedOutput); serializationObject.signalOutputs.push(serializedOutput); } } /** * Deserializes from an object * @param serializationObject the object to deserialize from */ deserialize(serializationObject) { for (let i = 0; i < serializationObject.signalInputs.length; i++) { const signalInput = this.getSignalInput(serializationObject.signalInputs[i].name); if (signalInput) { signalInput.deserialize(serializationObject.signalInputs[i]); } else { throw new Error("Could not find signal input with name " + serializationObject.signalInputs[i].name + " in block " + serializationObject.className); } } for (let i = 0; i < serializationObject.signalOutputs.length; i++) { const signalOutput = this.getSignalOutput(serializationObject.signalOutputs[i].name); if (signalOutput) { signalOutput.deserialize(serializationObject.signalOutputs[i]); } else { throw new Error("Could not find signal output with name " + serializationObject.signalOutputs[i].name + " in block " + serializationObject.className); } } } /** * @returns the class name */ getClassName() { return "FlowGraphExecutionBlock"; } } /** * This class is responsible for coordinating the events that are triggered in the scene. * It registers all observers needed to track certain events and triggers the blocks that are listening to them. * Abstracting the events from the class will allow us to easily change the events that are being listened to, and trigger them in any order. */ class FlowGraphSceneEventCoordinator { constructor(scene) { /** * register to this observable to get flow graph event notifications. */ this.onEventTriggeredObservable = new Observable(); /** * Was scene-ready already triggered? */ this.sceneReadyTriggered = false; this._pointerUnderMeshState = {}; this._startingTime = 0; this._scene = scene; this._initialize(); } _initialize() { this._sceneReadyObserver = this._scene.onReadyObservable.add(() => { if (!this.sceneReadyTriggered) { this.onEventTriggeredObservable.notifyObservers({ type: "SceneReady" /* FlowGraphEventType.SceneReady */ }); this.sceneReadyTriggered = true; } }); this._sceneDisposeObserver = this._scene.onDisposeObservable.add(() => { this.onEventTriggeredObservable.notifyObservers({ type: "SceneDispose" /* FlowGraphEventType.SceneDispose */ }); }); this._sceneOnBeforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => { const deltaTime = this._scene.getEngine().getDeltaTime() / 1000; // set in seconds this.onEventTriggeredObservable.notifyObservers({ type: "SceneBeforeRender" /* FlowGraphEventType.SceneBeforeRender */, payload: { timeSinceStart: this._startingTime, deltaTime, }, }); this._startingTime += deltaTime; }); this._meshPickedObserver = this._scene.onPointerObservable.add((pointerInfo) => { this.onEventTriggeredObservable.notifyObservers({ type: "MeshPick" /* FlowGraphEventType.MeshPick */, payload: pointerInfo }); }, PointerEventTypes.POINTERPICK); // should it be pointerdown? this._meshUnderPointerObserver = this._scene.onMeshUnderPointerUpdatedObservable.add((data) => { // check if the data has changed. Check the state of the last change and see if it is a mesh or null. // if it is a mesh and the previous state was null, trigger over event.