@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
459 lines • 18.7 kB
JavaScript
import { Observable } from "../Misc/observable.js";
import { FlowGraphContext } from "./flowGraphContext.js";
import { FlowGraphExecutionBlock } from "./flowGraphExecutionBlock.js";
import { FlowGraphAsyncExecutionBlock } from "./flowGraphAsyncExecutionBlock.js";
import { FlowGraphSceneEventCoordinator } from "./flowGraphSceneEventCoordinator.js";
import { _IsDescendantOf } from "./utils.js";
import { ValidateFlowGraphWithBlockList } from "./flowGraphValidator.js";
export var FlowGraphState;
(function (FlowGraphState) {
/**
* The graph is stopped
*/
FlowGraphState[FlowGraphState["Stopped"] = 0] = "Stopped";
/**
* The graph is running
*/
FlowGraphState[FlowGraphState["Started"] = 1] = "Started";
/**
* The graph is paused (contexts kept, pending tasks cancelled)
*/
FlowGraphState[FlowGraphState["Paused"] = 2] = "Paused";
})(FlowGraphState || (FlowGraphState = {}));
/**
* Class used to represent a flow graph.
* A flow graph is a graph of blocks that can be used to create complex logic.
* Blocks can be added to the graph and connected to each other.
* The graph can then be started, which will init and start all of its event blocks.
*
* @experimental FlowGraph is still in development and is subject to change.
*/
export class FlowGraph {
/**
* The scene associated with this flow graph.
*/
get scene() {
return this._scene;
}
/**
* The state of the graph
*/
get state() {
return this._state;
}
/**
* The state of the graph
*/
set state(value) {
this._state = value;
this.onStateChangedObservable.notifyObservers(value);
}
/**
* Construct a Flow Graph
* @param params construction parameters. currently only the scene
*/
constructor(params) {
/**
* An observable that is triggered when the state of the graph changes.
*/
this.onStateChangedObservable = new Observable();
/** @internal */
this._eventBlocks = {
["SceneReady" /* FlowGraphEventType.SceneReady */]: [],
["SceneDispose" /* FlowGraphEventType.SceneDispose */]: [],
["SceneBeforeRender" /* FlowGraphEventType.SceneBeforeRender */]: [],
["MeshPick" /* FlowGraphEventType.MeshPick */]: [],
["PointerDown" /* FlowGraphEventType.PointerDown */]: [],
["PointerUp" /* FlowGraphEventType.PointerUp */]: [],
["PointerMove" /* FlowGraphEventType.PointerMove */]: [],
["PointerOver" /* FlowGraphEventType.PointerOver */]: [],
["PointerOut" /* FlowGraphEventType.PointerOut */]: [],
["SceneAfterRender" /* FlowGraphEventType.SceneAfterRender */]: [],
["NoTrigger" /* FlowGraphEventType.NoTrigger */]: [],
};
/**
* All blocks that belong to this graph, including unreachable ones.
* @internal
*/
this._allBlocks = [];
this._executionContexts = [];
/**
* The state of the graph
*/
this._state = 0 /* FlowGraphState.Stopped */;
this._scene = params.scene;
this._sceneEventCoordinator = new FlowGraphSceneEventCoordinator(this._scene);
this._coordinator = params.coordinator;
}
_attachEventObserver() {
if (this._eventObserver) {
return;
}
this._eventObserver = this._sceneEventCoordinator.onEventTriggeredObservable.add((event) => {
if (event.type === "SceneDispose" /* FlowGraphEventType.SceneDispose */) {
this.dispose();
return;
}
if (this.state !== 1 /* FlowGraphState.Started */) {
return;
}
for (const context of this._executionContexts) {
const order = this._getContextualOrder(event.type, context);
for (const block of order) {
// iterate contexts
if (!block._executeEvent(context, event.payload)) {
break;
}
}
}
// custom behavior(s) of specific events
switch (event.type) {
case "SceneReady" /* FlowGraphEventType.SceneReady */:
this._sceneEventCoordinator.sceneReadyTriggered = true;
break;
case "SceneBeforeRender" /* FlowGraphEventType.SceneBeforeRender */:
for (const context of this._executionContexts) {
context._notifyOnTick(event.payload);
}
break;
}
});
}
_detachEventObserver() {
this._eventObserver?.remove();
this._eventObserver = null;
}
/**
* Sets a new scene for this flow graph, re-wiring all event listeners.
* This is useful when the scene the flow graph should listen to changes
* (e.g. when a new scene is loaded in an editor preview).
* If the graph is currently running, it will be stopped first and must be
* restarted manually after calling this method.
* @param scene the new scene to attach to
*/
setScene(scene) {
if (scene === this._scene) {
return;
}
if (this.state === 1 /* FlowGraphState.Started */) {
this.stop();
}
// Tear down old event coordinator
this._detachEventObserver();
this._sceneEventCoordinator.dispose();
// Rebuild with the new scene
this._scene = scene;
this._scene.constantlyUpdateMeshUnderPointer = true; // ensure pointer info is always up to date for event blocks that need it
this._sceneEventCoordinator = new FlowGraphSceneEventCoordinator(this._scene);
// Pre-attach the event observer so that events from the new
// coordinator are routed to the graph immediately. The handler
// guards against processing events while the graph is stopped,
// but having the observer in place ensures no events are lost
// when start() is called shortly after.
this._attachEventObserver();
}
/**
* Create a context. A context represents one self contained execution for the graph, with its own variables.
* @returns the context, where you can get and set variables
*/
createContext() {
const context = new FlowGraphContext({ scene: this._scene, coordinator: this._coordinator });
this._executionContexts.push(context);
return context;
}
/**
* Returns the execution context at a given index
* @param index the index of the context
* @returns the execution context at that index
*/
getContext(index) {
return this._executionContexts[index];
}
/**
* Returns all blocks registered in this graph, including disconnected ones.
* @returns a read-only array of all blocks
*/
getAllBlocks() {
return this._allBlocks;
}
/**
* Register a block with the graph. This does not wire any connections;
* it simply ensures the block is tracked so that serialization, editor
* display, and validation see it even when it is not reachable from an
* event block.
* @param block the block to register
*/
addBlock(block) {
if (this._allBlocks.indexOf(block) === -1) {
this._allBlocks.push(block);
}
}
/**
* Remove a block from the graph. Disconnects all of its ports and, if it
* is an event block, unregisters it from the event-block lists.
* @param block the block to remove
*/
removeBlock(block) {
const idx = this._allBlocks.indexOf(block);
if (idx !== -1) {
this._allBlocks.splice(idx, 1);
}
// If it is an event block, remove from the event-block registry
if (block instanceof FlowGraphExecutionBlock && "type" in block) {
const eventBlock = block;
const list = this._eventBlocks[eventBlock.type];
if (list) {
const eIdx = list.indexOf(eventBlock);
if (eIdx !== -1) {
list.splice(eIdx, 1);
}
}
}
// If the block has pending async tasks (e.g. event subscriptions),
// cancel them in all active execution contexts so deletion takes
// effect immediately even while the graph is running.
if (block instanceof FlowGraphAsyncExecutionBlock) {
for (const context of this._executionContexts) {
block._cancelPendingTasks(context);
block._resetAfterCanceled(context);
}
}
// Disconnect all ports
for (const input of block.dataInputs) {
input.disconnectFromAll();
}
for (const output of block.dataOutputs) {
output.disconnectFromAll();
}
if (block instanceof FlowGraphExecutionBlock) {
for (const signalIn of block.signalInputs) {
signalIn.disconnectFromAll();
}
for (const signalOut of block.signalOutputs) {
signalOut.disconnectFromAll();
}
}
}
/**
* Add an event block. When the graph is started, it will start listening to events
* from the block and execute the graph when they are triggered.
* @param block the event block to be added
*/
addEventBlock(block) {
this.addBlock(block);
if (block.type === "PointerOver" /* FlowGraphEventType.PointerOver */ || block.type === "PointerOut" /* FlowGraphEventType.PointerOut */) {
this._scene.constantlyUpdateMeshUnderPointer = true;
}
this._eventBlocks[block.type].push(block);
// if already started, sort and add to the pending
if (this.state === 1 /* FlowGraphState.Started */) {
for (const context of this._executionContexts) {
block._startPendingTasks(context);
}
}
else {
this.onStateChangedObservable.addOnce((state) => {
if (state === 1 /* FlowGraphState.Started */) {
for (const context of this._executionContexts) {
block._startPendingTasks(context);
}
}
});
}
}
/**
* Stops the flow graph. Cancels all pending tasks and clears execution contexts,
* but keeps event blocks so the graph can be restarted.
*/
stop() {
if (this.state === 0 /* FlowGraphState.Stopped */) {
return;
}
this._detachEventObserver();
this.state = 0 /* FlowGraphState.Stopped */;
for (const context of this._executionContexts) {
context._clearPendingBlocks();
context._clearPendingActivation();
}
this._executionContexts.length = 0;
}
/**
* Pauses the flow graph. Cancels pending tasks but keeps execution contexts and event blocks.
* Call start() to resume.
*/
pause() {
if (this.state !== 1 /* FlowGraphState.Started */) {
return;
}
this._detachEventObserver();
this.state = 2 /* FlowGraphState.Paused */;
for (const context of this._executionContexts) {
context._clearPendingBlocks();
}
}
/**
* Starts the flow graph. Initializes the event blocks and starts listening to events.
* Can also be called to resume from a paused state.
*/
start() {
if (this.state === 1 /* FlowGraphState.Started */) {
return;
}
const resumingFromPause = this.state === 2 /* FlowGraphState.Paused */;
if (this._executionContexts.length === 0) {
this.createContext();
}
this._attachEventObserver();
this.state = 1 /* FlowGraphState.Started */;
this._startPendingEvents();
// On a fresh start (not resume), fire the SceneReady event.
// The coordinator's own scene-ready observer may have already
// fired (and been lost) while the graph was stopped, so reset
// the flag and handle the ready state ourselves.
if (!resumingFromPause) {
this._sceneEventCoordinator.sceneReadyTriggered = false;
if (this._scene.isReady(true)) {
this._sceneEventCoordinator.sceneReadyTriggered = true;
this._sceneEventCoordinator.onEventTriggeredObservable.notifyObservers({ type: "SceneReady" /* FlowGraphEventType.SceneReady */ });
}
else {
// Scene isn't ready yet (e.g. pending shader compilations after
// a scene swap). Use executeWhenReady(true) which restarts the
// readiness check loop — a plain addOnce on onReadyObservable
// may never fire if the check loop already completed.
this._scene.executeWhenReady(() => {
if (this.state === 1 /* FlowGraphState.Started */ && !this._sceneEventCoordinator.sceneReadyTriggered) {
this._sceneEventCoordinator.sceneReadyTriggered = true;
this._sceneEventCoordinator.onEventTriggeredObservable.notifyObservers({ type: "SceneReady" /* FlowGraphEventType.SceneReady */ });
}
}, true);
}
}
}
_startPendingEvents() {
for (const context of this._executionContexts) {
for (const type in this._eventBlocks) {
const order = this._getContextualOrder(type, context);
for (const block of order) {
block._startPendingTasks(context);
}
}
}
}
_getContextualOrder(type, context) {
const order = this._eventBlocks[type].sort((a, b) => b.initPriority - a.initPriority);
if (type === "MeshPick" /* FlowGraphEventType.MeshPick */) {
const meshPickOrder = [];
for (const block1 of order) {
// If the block is a mesh pick, guarantee that picks of children meshes come before picks of parent meshes
const mesh1 = block1.asset.getValue(context);
let i = 0;
for (; i < order.length; i++) {
const block2 = order[i];
const mesh2 = block2.asset.getValue(context);
if (mesh1 && mesh2 && _IsDescendantOf(mesh1, mesh2)) {
break;
}
}
meshPickOrder.splice(i, 0, block1);
}
return meshPickOrder;
}
return order;
}
/**
* Disposes of the flow graph. Cancels any pending tasks and removes all event listeners.
*/
dispose() {
if (this.state === 0 /* FlowGraphState.Stopped */) {
return;
}
this.state = 0 /* FlowGraphState.Stopped */;
for (const context of this._executionContexts) {
context._clearPendingBlocks();
context._clearPendingActivation();
}
this._executionContexts.length = 0;
for (const type in this._eventBlocks) {
this._eventBlocks[type].length = 0;
}
this._allBlocks.length = 0;
this._detachEventObserver();
this._sceneEventCoordinator.dispose();
}
/**
* Executes a function in all blocks of a flow graph, starting with the event blocks.
* @param visitor the function to execute.
*/
visitAllBlocks(visitor) {
const visitList = [];
const idsAddedToVisitList = new Set();
for (const type in this._eventBlocks) {
for (const block of this._eventBlocks[type]) {
visitList.push(block);
idsAddedToVisitList.add(block.uniqueId);
}
}
while (visitList.length > 0) {
const block = visitList.pop();
visitor(block);
for (const dataIn of block.dataInputs) {
for (const connection of dataIn._connectedPoint) {
if (!idsAddedToVisitList.has(connection._ownerBlock.uniqueId)) {
visitList.push(connection._ownerBlock);
idsAddedToVisitList.add(connection._ownerBlock.uniqueId);
}
}
}
if (block instanceof FlowGraphExecutionBlock) {
for (const signalOut of block.signalOutputs) {
for (const connection of signalOut._connectedPoint) {
if (!idsAddedToVisitList.has(connection._ownerBlock.uniqueId)) {
visitList.push(connection._ownerBlock);
idsAddedToVisitList.add(connection._ownerBlock.uniqueId);
}
}
}
}
}
}
/**
* Validates the flow graph and returns all issues found.
* Uses the tracked block list for complete validation including unreachable block detection.
* @returns The validation result containing errors and warnings.
*/
validate() {
return ValidateFlowGraphWithBlockList(this, this._allBlocks);
}
/**
* Serializes a graph
* @param serializationObject the object to write the values in
* @param valueSerializeFunction a function to serialize complex values
*/
serialize(serializationObject = {}, valueSerializeFunction) {
serializationObject.allBlocks = [];
// Collect all blocks: traversal-reachable ones plus any registered
// orphans in _allBlocks (e.g. disconnected blocks in the editor).
const seen = new Set();
const serializeBlock = (block) => {
if (seen.has(block.uniqueId)) {
return;
}
seen.add(block.uniqueId);
const serializedBlock = {};
block.serialize(serializedBlock);
serializationObject.allBlocks.push(serializedBlock);
};
this.visitAllBlocks(serializeBlock);
for (const block of this._allBlocks) {
serializeBlock(block);
}
serializationObject.executionContexts = [];
for (const context of this._executionContexts) {
const serializedContext = {};
context.serialize(serializedContext, valueSerializeFunction);
serializationObject.executionContexts.push(serializedContext);
}
}
}
//# sourceMappingURL=flowGraph.js.map