@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.
324 lines • 14.5 kB
JavaScript
import { FlowGraphExecutionBlock } from "./flowGraphExecutionBlock.js";
import { FlowGraphEventBlock } from "./flowGraphEventBlock.js";
/**
* Severity level for a validation issue.
*/
export var FlowGraphValidationSeverity;
(function (FlowGraphValidationSeverity) {
/** A critical issue that will cause runtime failure. */
FlowGraphValidationSeverity[FlowGraphValidationSeverity["Error"] = 0] = "Error";
/** A potential issue that may indicate a mistake. */
FlowGraphValidationSeverity[FlowGraphValidationSeverity["Warning"] = 1] = "Warning";
})(FlowGraphValidationSeverity || (FlowGraphValidationSeverity = {}));
// Types that are mutually coercible and should not produce type-mismatch warnings
const NumericLikeTypes = new Set(["number" /* FlowGraphTypes.Number */, "FlowGraphInteger" /* FlowGraphTypes.Integer */, "boolean" /* FlowGraphTypes.Boolean */]);
const VectorLikeTypes = new Set(["Vector4" /* FlowGraphTypes.Vector4 */, "Quaternion" /* FlowGraphTypes.Quaternion */]);
const ColorLikeTypes = new Set(["Color3" /* FlowGraphTypes.Color3 */, "Color4" /* FlowGraphTypes.Color4 */]);
/**
* @internal
*/
function _AreTypesCompatible(sourceType, targetType) {
if (sourceType === targetType) {
return true;
}
// "any" is compatible with everything
if (sourceType === "any" /* FlowGraphTypes.Any */ || targetType === "any" /* FlowGraphTypes.Any */) {
return true;
}
// Numeric coercion group
if (NumericLikeTypes.has(sourceType) && NumericLikeTypes.has(targetType)) {
return true;
}
// Vector4 / Quaternion are interchangeable
if (VectorLikeTypes.has(sourceType) && VectorLikeTypes.has(targetType)) {
return true;
}
// Color3 → Color4 widening is safe
if (ColorLikeTypes.has(sourceType) && ColorLikeTypes.has(targetType)) {
return true;
}
return false;
}
/**
* Validates a flow graph and returns all issues found.
*
* The following checks are performed:
* 1. **No event blocks** — the graph has no entry points.
* 2. **Unconnected required data inputs** — a non-optional data input with no connection.
* 3. **Unconnected signal inputs** — an execution block whose `in` signal has no connection and
* that is not an event block (entry point).
* 4. **Data type mismatches** — a data connection whose source richType is incompatible with the
* target richType.
* 5. **Unreachable blocks** — blocks not reachable from any event block via signal/data traversal.
* 6. **Data dependency cycles** — circular data-only connections that would cause infinite recursion.
*
* @param flowGraph - The flow graph to validate.
* @returns The validation result.
*/
export function ValidateFlowGraph(flowGraph) {
const issues = [];
const issuesByBlock = new Map();
const addIssue = (issue) => {
issues.push(issue);
if (issue.block) {
let arr = issuesByBlock.get(issue.block.uniqueId);
if (!arr) {
arr = [];
issuesByBlock.set(issue.block.uniqueId, arr);
}
arr.push(issue);
}
};
// Collect ALL blocks via visitAllBlocks
const allBlocks = [];
flowGraph.visitAllBlocks((block) => {
allBlocks.push(block);
});
// ── Check 1: No event blocks ───────────────────────────────────────
const eventBlocks = _GetEventBlocks(flowGraph);
if (eventBlocks.length === 0) {
addIssue({
severity: 0 /* FlowGraphValidationSeverity.Error */,
message: "Graph has no event blocks — nothing will trigger execution.",
});
}
// ── Check 2: Unconnected required data inputs ──────────────────────
for (const block of allBlocks) {
for (const input of block.dataInputs) {
if (!input.optional && !input.isDisabled && !input.isConnected()) {
addIssue({
severity: 1 /* FlowGraphValidationSeverity.Warning */,
message: `"${input.name}" is not connected and will use its default value.`,
block,
connectionName: input.name,
});
}
}
}
// ── Check 3: Unconnected signal inputs on non-event execution blocks
for (const block of allBlocks) {
if (block instanceof FlowGraphExecutionBlock) {
// Skip event blocks — they are entry points and don't need an incoming signal.
if (_IsEventBlock(block)) {
continue;
}
const inSignal = block.signalInputs.find((s) => s.name === "in");
if (inSignal && !inSignal.isConnected()) {
addIssue({
severity: 0 /* FlowGraphValidationSeverity.Error */,
message: `Execution block has no incoming signal — it will never execute.`,
block,
connectionName: "in",
});
}
}
}
// ── Check 4: Data type mismatches ──────────────────────────────────
for (const block of allBlocks) {
for (const input of block.dataInputs) {
if (!input.isConnected()) {
continue;
}
const source = input._connectedPoint[0];
const srcType = source.richType?.typeName;
const tgtType = input.richType?.typeName;
if (srcType && tgtType && !_AreTypesCompatible(srcType, tgtType)) {
// If either side has a typeTransformer, it's intentionally converted
if (source.richType.typeTransformer !== undefined || input.richType.typeTransformer !== undefined) {
continue;
}
addIssue({
severity: 1 /* FlowGraphValidationSeverity.Warning */,
message: `Type mismatch: "${source._ownerBlock.name}.${source.name}" (${srcType}) → "${block.name}.${input.name}" (${tgtType}).`,
block,
connectionName: input.name,
});
}
}
}
// ── Check 5: Unreachable blocks ────────────────────────────────────
const reachableIds = new Set();
flowGraph.visitAllBlocks((block) => {
reachableIds.add(block.uniqueId);
});
// We need to also check standalone data blocks not visited by visitAllBlocks.
// visitAllBlocks starts from event blocks; any block not found is unreachable.
// Since we can only iterate blocks we know about, the allBlocks list IS
// from visitAllBlocks, so everything in it IS reachable.
// There's no separate registry of "all added blocks" in the FlowGraph.
// So: unreachable blocks are blocks that exist but aren't visited.
// Currently visitAllBlocks IS our source, so this check is a no-op for
// blocks added via the graph. However, the editor creates nodes and adds
// execution blocks, so we should compare against the editor's full list.
// For the core validator, we expose a variant that accepts an external block list.
// ── Check 6: Data dependency cycles ────────────────────────────────
_DetectDataCycles(allBlocks, addIssue);
// Sort: errors first, then warnings
issues.sort((a, b) => a.severity - b.severity);
return {
isValid: issues.every((i) => i.severity !== 0 /* FlowGraphValidationSeverity.Error */),
issues,
errorCount: issues.filter((i) => i.severity === 0 /* FlowGraphValidationSeverity.Error */).length,
warningCount: issues.filter((i) => i.severity === 1 /* FlowGraphValidationSeverity.Warning */).length,
issuesByBlock,
};
}
/**
* Extended validation that also checks for unreachable blocks.
* Requires a full list of all blocks in the graph (including those not reachable
* from event blocks via the normal traversal).
*
* @param flowGraph - The flow graph to validate.
* @param allKnownBlocks - Complete list of all blocks (e.g., from the editor's node set).
* @returns The validation result.
*/
export function ValidateFlowGraphWithBlockList(flowGraph, allKnownBlocks) {
const result = ValidateFlowGraph(flowGraph);
// Check for unreachable blocks
const reachableIds = new Set();
flowGraph.visitAllBlocks((block) => {
reachableIds.add(block.uniqueId);
});
for (const block of allKnownBlocks) {
if (!reachableIds.has(block.uniqueId)) {
const issue = {
severity: 1 /* FlowGraphValidationSeverity.Warning */,
message: `Block is unreachable from any event block.`,
block,
};
result.issues.push(issue);
result.warningCount++;
let arr = result.issuesByBlock.get(block.uniqueId);
if (!arr) {
arr = [];
result.issuesByBlock.set(block.uniqueId, arr);
}
arr.push(issue);
// Also run Check 2 and Check 3 on unreachable blocks so the editor
// can display all issues, not just reachability ones.
for (const input of block.dataInputs) {
if (!input.optional && !input.isDisabled && !input.isConnected()) {
const dataIssue = {
severity: 1 /* FlowGraphValidationSeverity.Warning */,
message: `"${input.name}" is not connected and will use its default value.`,
block,
connectionName: input.name,
};
result.issues.push(dataIssue);
result.warningCount++;
arr.push(dataIssue);
}
}
if (block instanceof FlowGraphExecutionBlock && !_IsEventBlock(block)) {
const inSignal = block.signalInputs.find((s) => s.name === "in");
if (inSignal && !inSignal.isConnected()) {
const signalIssue = {
severity: 0 /* FlowGraphValidationSeverity.Error */,
message: `Execution block has no incoming signal — it will never execute.`,
block,
connectionName: "in",
};
result.issues.push(signalIssue);
result.errorCount++;
arr.push(signalIssue);
}
}
}
}
result.isValid = result.issues.every((i) => i.severity !== 0 /* FlowGraphValidationSeverity.Error */);
result.issues.sort((a, b) => a.severity - b.severity);
return result;
}
/**
* Get all event blocks from a flow graph.
* @param flowGraph - the flow graph
* @returns the event blocks
*/
function _GetEventBlocks(flowGraph) {
const eventBlocks = [];
for (const type in flowGraph._eventBlocks) {
for (const block of flowGraph._eventBlocks[type]) {
eventBlocks.push(block);
}
}
return eventBlocks;
}
/**
* Detect whether a block is an event block (entry point).
* @param block - the block to check
* @returns true if it is an event block
*/
function _IsEventBlock(block) {
return block instanceof FlowGraphEventBlock;
}
/**
* Detects cycles among data-only blocks.
* A data cycle means that block A's output feeds into block B's input, and
* block B's output feeds back into block A (directly or indirectly).
* This would cause infinite recursion during getValue().
* @param allBlocks - all blocks to check
* @param addIssue - callback to report issues
*/
function _DetectDataCycles(allBlocks, addIssue) {
// Build adjacency: for each block, which blocks do its data inputs depend on?
// (Data inputs pull values from connected output blocks.)
const white = 0; // unvisited
const gray = 1; // in current DFS path
const black = 2; // fully explored
const color = new Map();
for (const block of allBlocks) {
color.set(block.uniqueId, white);
}
const blockMap = new Map();
for (const block of allBlocks) {
blockMap.set(block.uniqueId, block);
}
const reportedCycleBlocks = new Set();
function dfs(block) {
color.set(block.uniqueId, gray);
for (const input of block.dataInputs) {
if (!input.isConnected()) {
continue;
}
for (const connected of input._connectedPoint) {
const dep = connected._ownerBlock;
// Only consider data-only blocks (not execution blocks which are driven by signals)
if (dep instanceof FlowGraphExecutionBlock) {
continue;
}
const depColor = color.get(dep.uniqueId);
if (depColor === gray) {
// Cycle found
if (!reportedCycleBlocks.has(block.uniqueId)) {
reportedCycleBlocks.add(block.uniqueId);
addIssue({
severity: 0 /* FlowGraphValidationSeverity.Error */,
message: `Data dependency cycle detected — getValue() will recurse infinitely.`,
block,
});
}
if (!reportedCycleBlocks.has(dep.uniqueId)) {
reportedCycleBlocks.add(dep.uniqueId);
addIssue({
severity: 0 /* FlowGraphValidationSeverity.Error */,
message: `Data dependency cycle detected — getValue() will recurse infinitely.`,
block: dep,
});
}
return true;
}
if (depColor === white) {
dfs(dep);
}
}
}
color.set(block.uniqueId, black);
return false;
}
for (const block of allBlocks) {
if (color.get(block.uniqueId) === white) {
dfs(block);
}
}
}
//# sourceMappingURL=flowGraphValidator.js.map