@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
JavaScript
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.