Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
fcf2c8c
KHR_Interactivity importer: squash all work from importers/khr-intera…
SergioRZMasson Apr 27, 2026
af00b9d
KHR_interactivity: support opaque-reference (ref) spec update
SergioRZMasson Apr 27, 2026
ce12e03
Merge remote-tracking branch 'origin/master' into khr-interactivity-i…
SergioRZMasson Apr 28, 2026
d204462
KHR_interactivity: composite path converter + Babylon scene namespace
SergioRZMasson Apr 28, 2026
82ab3fc
KHR_interactivity: drop inspector-v2 CLI WIP leftovers from prior squash
SergioRZMasson Apr 28, 2026
947ec8b
KHR_interactivity: expand morph-target weights pointer support
SergioRZMasson May 1, 2026
591773b
KHR_interactivity: fix build/lint/format errors from morph-target update
SergioRZMasson May 1, 2026
91d04bc
KHR_interactivity: glTF-side fallback for KHR_materials_volume textur…
SergioRZMasson May 1, 2026
4cb80d5
KHR_interactivity: glTF-side fallback for transmission texture transf…
SergioRZMasson May 1, 2026
3bfe531
KHR_interactivity: bake static ref prefix into relative pointer templ…
SergioRZMasson May 1, 2026
aff0c97
KHR_interactivity: implement ref/eq operation
SergioRZMasson May 1, 2026
4073366
KHR_interactivity: preserve raw anisotropyRotation across pointer/set…
SergioRZMasson May 4, 2026
56d4470
Revert "KHR_interactivity: preserve raw anisotropyRotation across poi…
SergioRZMasson May 4, 2026
4767276
KHR_interactivity: implement math/quatSlerp operation
SergioRZMasson May 4, 2026
bcb8157
KHR_interactivity: expose selectedNode ref output on event/onSelect
SergioRZMasson May 4, 2026
254d986
KHR_interactivity: resolve Babylon-object refs in pointer templates
SergioRZMasson May 4, 2026
2082c69
KHR_interactivity: return ref strings from read-only glTF tree accessors
SergioRZMasson May 5, 2026
f75563f
FlowGraph: normalise trailing slash for ref-typed string equality
SergioRZMasson May 5, 2026
4e75d67
KHR_interactivity: expose hoveredNode ref output on event/onHoverIn a…
SergioRZMasson May 6, 2026
14e7cfb
sandbox: load CDN scripts with crossOrigin=anonymous
SergioRZMasson May 6, 2026
7644ae6
KHR_interactivity: handle ref-typed animation refs in pointer/get and…
SergioRZMasson May 6, 2026
94ae801
KHR_interactivity: resolve trailing-slash refs and animation pointers…
SergioRZMasson May 6, 2026
989093c
KHR_interactivity: support dynamic refs in relative pointer templates
SergioRZMasson May 6, 2026
e04454e
KHR_node_visibility: apply `visible: false` to primitive child meshes…
SergioRZMasson May 6, 2026
be979ed
FlowGraph: type/intToFloat tolerates plain-number inputs in addition …
SergioRZMasson May 7, 2026
0df1961
Revert sandbox/babylonServer/viewer changes (unrelated to KHR_interac…
SergioRZMasson May 13, 2026
755639e
Merge remote-tracking branch 'origin/master' into khr-interactivity-i…
SergioRZMasson May 13, 2026
03c6be1
Normalize objectModelMapping.ts line endings to LF
SergioRZMasson May 13, 2026
2eb2839
Remove ancillary docs/gitignore not needed for the PR
SergioRZMasson May 13, 2026
1467d25
Remove integration tests from PR scope
SergioRZMasson May 13, 2026
5d88220
Fix: ref-typed socket detection in pointer bake (Calculator/Puzzle)
SergioRZMasson May 13, 2026
2614c14
Fix sandbox: register glTF 2.0 extensions + LoadingScreen side-effect
SergioRZMasson May 13, 2026
8c317cb
Add JSDoc comments on public interactivity / object-model declarations
SergioRZMasson May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,29 @@ export class FlowGraphRandomBlock extends FlowGraphConstantOperationBlock<FlowGr
const max = this.max.getValue(context);
return this._getRandomValue() * (max - min) + min;
}

/**
* Override to use per-execution caching that generates a fresh value for each
* new signal execution, while returning the same value when read multiple times
* within the same execution (required by the KHR_Interactivity spec).
* @param context the graph context
*/
public override _updateOutputs(context: FlowGraphContext) {
const cachedExecId = context._getExecutionVariable(this, "_randomExecId", -1);
if (cachedExecId === context.executionId) {
// Same execution — return the cached random value
return;
}
// New execution — generate a fresh random value
const calculatedValue = this._doOperation(context);
if (calculatedValue === undefined || calculatedValue === null) {
this.isValid.setValue(false, context);
return;
}
context._setExecutionVariable(this, "_randomExecId", context.executionId);
this.value.setValue(calculatedValue, context);
this.isValid.setValue(true, context);
}
}
RegisterClass(FlowGraphBlockNames.Random, FlowGraphRandomBlock);

Expand Down Expand Up @@ -708,6 +731,23 @@ export class FlowGraphMathInterpolationBlock extends FlowGraphTernaryOperationBl
}
RegisterClass(FlowGraphBlockNames.MathInterpolation, FlowGraphMathInterpolationBlock);

/**
* Spherical linear interpolation between two unit quaternions, matching the
* KHR_interactivity `math/quatSlerp` operation. The two inputs are treated as
* Babylon `Quaternion` values regardless of their concrete class (the spec
* defines them as `float4`), and the output is a `Quaternion`.
*
* The underlying implementation is `Quaternion.Slerp`, whose algorithm matches
* the spec step-for-step (dot product, conditional sign-flip on `b`, sin-based
* weighting, and a near-identity short-circuit to a linear blend).
*/
export class FlowGraphMathSlerpBlock extends FlowGraphTernaryOperationBlock<Quaternion, Quaternion, number, Quaternion> {
constructor(config?: IFlowGraphBlockConfiguration) {
super(RichTypeAny, RichTypeAny, RichTypeNumber, RichTypeAny, (a, b, c) => Quaternion.Slerp(a, b, c), FlowGraphBlockNames.MathSlerp, config);
}
}
RegisterClass(FlowGraphBlockNames.MathSlerp, FlowGraphMathSlerpBlock);

/**
* Equals block.
*/
Expand All @@ -719,14 +759,31 @@ export class FlowGraphEqualityBlock extends FlowGraphBinaryOperationBlock<FlowGr
private _polymorphicEq(a: FlowGraphMathOperationType, b: FlowGraphMathOperationType) {
const aClassName = _GetClassNameOf(a);
const bClassName = _GetClassNameOf(b);
if (_AreSameVectorOrQuaternionClass(aClassName, bClassName) || _AreSameMatrixClass(aClassName, bClassName) || _AreSameIntegerClass(aClassName, bClassName)) {
return (a as Vector3).equals(b as Vector3);
}
// Handle mixed number/FlowGraphInteger comparison
if (isNumeric(a) && isNumeric(b)) {
return getNumericValue(a as FlowGraphNumber) === getNumericValue(b as FlowGraphNumber);
}
if (typeof a !== typeof b) {
return false;
}
if (_AreSameVectorOrQuaternionClass(aClassName, bClassName) || _AreSameMatrixClass(aClassName, bClassName) || _AreSameIntegerClass(aClassName, bClassName)) {
return (a as Vector3).equals(b as Vector3);
} else {
return a === b;
// Both sides are JSON-Pointer-like ref strings ("/foo/0", "/foo/0/", ...).
// KHR_interactivity ref/eq is defined as "refers to the same object",
// so normalise trailing slashes before comparing — different asset
// authors emit refs with and without the trailing slash for the same
// object (e.g. /nodes/420 vs /nodes/420/).
if (typeof a === "string" && typeof b === "string") {
const aStr: string = a;
const bStr: string = b;
if (aStr.length > 0 && aStr[0] === "/" && bStr.length > 0 && bStr[0] === "/") {
const ar = aStr.endsWith("/") ? aStr.slice(0, -1) : aStr;
const br = bStr.endsWith("/") ? bStr.slice(0, -1) : bStr;
return ar === br;
}
}
return a === b;
}
}
RegisterClass(FlowGraphBlockNames.Equality, FlowGraphEqualityBlock);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export class FlowGraphJsonPointerParserBlock<P extends any, O extends FlowGraphA
if (type.startsWith("Color")) {
value = ToColor(value as Vector4, type) as unknown as P;
}
// Unwrap FlowGraphInteger to plain number for numeric setters
if (typeof (value as any)?.value === "number" && (value as any)?.getClassName?.() === "FlowGraphInteger") {
value = (value as any).value as unknown as P;
}
accessorContainer.info.set?.(value, accessorContainer.object);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type IFlowGraphBlockConfiguration } from "core/FlowGraph/flowGraphBlock";
import { FlowGraphUnaryOperationBlock } from "../flowGraphUnaryOperationBlock";
import { RichTypeBoolean, RichTypeFlowGraphInteger, RichTypeNumber } from "core/FlowGraph/flowGraphRichTypes";
import { RichTypeAny, RichTypeBoolean, RichTypeFlowGraphInteger, RichTypeNumber } from "core/FlowGraph/flowGraphRichTypes";
import { FlowGraphBlockNames } from "../../flowGraphBlockNames";
import { RegisterClass } from "core/Misc/typeStore";
import { FlowGraphInteger } from "core/FlowGraph/CustomTypes/flowGraphInteger";
Expand Down Expand Up @@ -51,10 +51,19 @@ RegisterClass(FlowGraphBlockNames.IntToBoolean, FlowGraphIntToBoolean);

/**
* A block that converts an integer to a float.
*
* KHR_interactivity ``type/intToFloat`` is spec-defined to take an ``int``
* input. The Babylon FlowGraph runtime represents ``int`` as
* ``FlowGraphInteger`` (a wrapper carrying ``.value``). Accept both shapes —
* a plain JavaScript ``number`` and a ``FlowGraphInteger`` — so accessors that
* return a raw integer (e.g. the ``/nodes/{}/children.length`` pointer/get
* accessor in ``objectModelMapping.ts``) interoperate with downstream graph
* blocks without producing ``undefined`` (which then poisons math chains
* with NaN).
*/
export class FlowGraphIntToFloat extends FlowGraphUnaryOperationBlock<FlowGraphInteger, number> {
export class FlowGraphIntToFloat extends FlowGraphUnaryOperationBlock<FlowGraphInteger | number, number> {
constructor(config?: IFlowGraphBlockConfiguration) {
super(RichTypeFlowGraphInteger, RichTypeNumber, (a) => a.value, FlowGraphBlockNames.IntToFloat, config);
super(RichTypeAny, RichTypeNumber, (a) => (typeof a === "number" ? a : a?.value), FlowGraphBlockNames.IntToFloat, config);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,27 @@ export class FlowGraphArrayIndexBlock<T = any> extends FlowGraphBlock {
*/
public override _updateOutputs(context: FlowGraphContext): void {
const array = this.array.getValue(context);
const index = getNumericValue(this.index.getValue(context));
const rawIndex = this.index.getValue(context);
// KHR_interactivity opaque-reference values feed in here as JSON-Pointer
// ref strings (e.g. "/animations/0/") instead of plain integers. Extract
// the trailing numeric segment as the index in that case. Undefined /
// unconnected inputs short-circuit to a null output instead of crashing
// ``getNumericValue`` on a missing ``.value`` property.
let index: number;
if (rawIndex === undefined || rawIndex === null) {
this.value.setValue(null, context);
return;
}
if (typeof rawIndex === "string") {
const parsed = _ParseRefIndex(rawIndex);
if (parsed === undefined) {
this.value.setValue(null, context);
return;
}
index = parsed;
} else {
index = getNumericValue(rawIndex);
}
if (array && index >= 0 && index < array.length) {
this.value.setValue(array[index], context);
} else {
Expand All @@ -66,3 +86,28 @@ export class FlowGraphArrayIndexBlock<T = any> extends FlowGraphBlock {
}

RegisterClass(FlowGraphBlockNames.ArrayIndex, FlowGraphArrayIndexBlock);

/**
* Extract the trailing numeric segment from a JSON-Pointer-shaped ref string,
* e.g. ``/animations/0/`` → 0, ``/nodes/12`` → 12. Returns ``undefined`` if the
* string does not match the expected ``/<container>/<index>(/?)`` shape.
* @param ref the ref string to parse.
* @returns the trailing integer segment, or ``undefined`` when the input is
* not a JSON-Pointer-shaped ref ending in an integer.
*/
function _ParseRefIndex(ref: string): number | undefined {
if (ref.length === 0 || ref[0] !== "/") {
return undefined;
}
const trimmed = ref.endsWith("/") ? ref.slice(0, -1) : ref;
const lastSlash = trimmed.lastIndexOf("/");
if (lastSlash < 0) {
return undefined;
}
const tail = trimmed.substring(lastSlash + 1);
if (tail.length === 0) {
return undefined;
}
const parsed = Number(tail);
return Number.isFinite(parsed) && Number.isInteger(parsed) ? parsed : undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export class FlowGraphReceiveCustomEventBlock extends FlowGraphEventBlock {
const typeKey = typeof entry.type === "string" ? entry.type : entry.type?.typeName;
const richType = typeof entry.type?.serialize === "function" ? entry.type : getRichTypeByFlowGraphType(typeKey);
entry.type = richType;
this.registerDataOutput(key, richType);
// Pass default value from event data schema so outputs have the correct initial value
this.registerDataOutput(key, richType, (entry as any).value);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,31 @@ export class FlowGraphPlayAnimationBlock extends FlowGraphAsyncExecutionBlock {
const from = this.from.getValue(context) ?? 0;
// not accepting 0
const to = this.to.getValue(context) || animationGroupToUse.to;

// Validate duration for interpolation animations: non-finite or negative values trigger the error flow.
// Only validate when animation is provided (interpolation case), not for general animation/start
// where the AnimationGroup's default to value may legitimately be negative.
if (!isFinite(to) || !isFinite(from)) {
return this._reportError(context, "Invalid animation duration");
}
if (animation && to < 0) {
return this._reportError(context, "Invalid animation duration");
}

// Validate easing function: NaN bezier control points trigger the error flow
if (animation) {
const animationsArray = Array.isArray(animation) ? animation : [animation];
for (const anim of animationsArray) {
const easing = anim.getEasingFunction?.();
if (easing && "x1" in easing) {
const bezier = easing as unknown as { x1: number; y1: number; x2: number; y2: number };
if (isNaN(bezier.x1) || isNaN(bezier.y1) || isNaN(bezier.x2) || isNaN(bezier.y2)) {
return this._reportError(context, "Invalid bezier curve control points");
}
}
}
}

const loop = !isFinite(to) || this.loop.getValue(context);
this.currentAnimationGroup.setValue(animationGroupToUse, context);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export function blockFactory(blockName: FlowGraphBlockNames | string): () => Pro
return async () => (await import("./Data/Math/flowGraphMathBlocks")).FlowGraphSaturateBlock;
case FlowGraphBlockNames.MathInterpolation:
return async () => (await import("./Data/Math/flowGraphMathBlocks")).FlowGraphMathInterpolationBlock;
case FlowGraphBlockNames.MathSlerp:
return async () => (await import("./Data/Math/flowGraphMathBlocks")).FlowGraphMathSlerpBlock;
case FlowGraphBlockNames.Equality:
return async () => (await import("./Data/Math/flowGraphMathBlocks")).FlowGraphEqualityBlock;
case FlowGraphBlockNames.LessThan:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const enum FlowGraphBlockNames {
Clamp = "FlowGraphClampBlock",
Saturate = "FlowGraphSaturateBlock",
MathInterpolation = "FlowGraphMathInterpolationBlock",
MathSlerp = "FlowGraphMathSlerpBlock",
Equality = "FlowGraphEqualityBlock",
LessThan = "FlowGraphLessThanBlock",
LessThanOrEqual = "FlowGraphLessThanOrEqualBlock",
Expand Down
Loading
Loading