Skip to content

Commit 0ff0557

Browse files
authored
FGE Phase 2: Variables panel improvements, autocomplete picker, runtime values & tests (#18310)
## Summary Improvements to the Flow Graph Editor variables panel: live runtime value display, UX bug fixes, autocomplete variable picker, and comprehensive unit tests. ## Changes ### Features - **Live runtime variable values**: While the flow graph is running, variables panel polls context values every 200ms and displays them with a green "● Live" badge - **AutoComplete variable picker**: Replaced the confusing dual dropdown+text input with a single autocomplete input that filters existing variable names and allows creating new ones - **Variable-picker config kind**: Added `variable-picker` to the constructor config registry so Get/Set Variable blocks use the new autocomplete picker ### Bug Fixes - **New variable disappearing on blur**: Changed from `fg.start()/fg.stop()` (which destroys contexts) to `fg.createContext()` for adding variables - **Shared React state between panels**: Added `key={uniqueId}` to `React.createElement` in `graphNode.ts` so React doesn't reuse component instances across different blocks of the same type - **Backspace deleting blocks while typing**: Added `lockObject` support to AutoCompleteInputComponent to prevent graph canvas key handlers from firing during text input - **Orphan blocks not scanned**: Switched from `visitAllBlocks()` (traversal-based) to `getAllBlocks()` (includes disconnected blocks) in variable utils ### Testing & Refactoring - Extracted pure logic into `variableUtils.ts` for testability: `gatherVariables`, `gatherVariableNames`, `renameVariable`, `deleteVariable`, `formatVariableValue`, `filterSuggestions` - Added **50 unit tests** covering all utility functions, constructor config registry entries, and context persistence integration ## Files Changed | File | Change | |------|--------| | `variableUtils.ts` | **New** — extracted pure logic functions | | `autoCompleteInputComponent.tsx` | **New** — autocomplete input with lockObject | | `autoComplete.scss` | **New** — autocomplete styles | | `variableUtils.test.ts` | **New** — 50 unit tests | | `variablesPanelComponent.tsx` | Refactored to use variableUtils, added runtime value display | | `genericNodePropertyComponent.tsx` | Uses `gatherVariableNames` from variableUtils | | `setVariablePropertyComponent.tsx` | Uses `gatherVariableNames` + autocomplete picker | | `constructorConfigRegistry.ts` | Added `variable-picker` kind | | `graphNode.ts` | Added `key: uniqueId` to createElement | | `variables.scss` | Updated for runtime value display + Live badge | | `tsconfig.build.json` | Updated paths |
1 parent 6383f70 commit 0ff0557

19 files changed

Lines changed: 1785 additions & 37 deletions

packages/dev/sharedUiComponents/src/nodeGraphSystem/graphNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ export class GraphNode {
708708
control = PropertyLedger.DefaultControl;
709709
}
710710

711-
return React.createElement(control, { stateManager: this._stateManager, nodeData: this.content });
711+
return React.createElement(control, { key: this.content.uniqueId, stateManager: this._stateManager, nodeData: this.content });
712712
}
713713

714714
public _forceRebuild(source: any, propertyName: string, notifiers?: IEditablePropertyOption["notifiers"]) {

packages/tools/flowGraphEditor/ISSUES-Phase-2.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ When done with an issue, update the MANUAL.md to reflect the new feature or fix,
1616

1717
## High (significantly impacts experience)
1818

19-
- [ ] **No "How to Use" code export dialog** — After saving a flow graph (to file or snippet), there is no guidance on how to actually load and run it in a user's own scene. A user who builds a flow graph in the editor has no idea what code to write to consume it. **Expected:** A "How to Use" / "Embed" button in the toolbar (next to Save File / Save Snippet) that opens a dialog with copy-to-clipboard code samples showing both integration methods:
19+
- [x] **No "How to Use" code export dialog** — After saving a flow graph (to file or snippet), there is no guidance on how to actually load and run it in a user's own scene. A user who builds a flow graph in the editor has no idea what code to write to consume it. **Expected:** A "How to Use" / "Embed" button in the toolbar (next to Save File / Save Snippet) that opens a dialog with copy-to-clipboard code samples showing both integration methods:
2020
- **From snippet:** `FlowGraph.ParseFromSnippetAsync("<snippetId>", scene)` — pre-filled with the current snippet ID if the graph has been saved to the snippet server.
2121
- **From JSON file:** Loading the saved `.json` file and calling `FlowGraph.ParseFlowGraphAsync(data, { scene })`.
2222

2323
The dialog should include minimal but complete boilerplate (import statements, async wrapper) so users can paste the code directly into their project.
2424

25-
- [ ] **No variables panel** — There is no way to see all graph variables at a glance, set initial values, rename them globally, or inspect current runtime values. Users must create `GetVariable`/`SetVariable` blocks and hope the variable names match across the graph. A typo in a variable name silently creates a separate variable. **Expected:** A "Variables" panel listing all variables defined in the graph with their name, type, and default value. Users should be able to add, rename, and delete variables from this panel. Renaming should propagate to all `GetVariable`/`SetVariable` blocks referencing that name.
25+
- [x] **No variables panel** — There is no way to see all graph variables at a glance, set initial values, rename them globally, or inspect current runtime values. Users must create `GetVariable`/`SetVariable` blocks and hope the variable names match across the graph. A typo in a variable name silently creates a separate variable. **Expected:** A "Variables" panel listing all variables defined in the graph with their name, type, and default value. Users should be able to add, rename, and delete variables from this panel. Renaming should propagate to all `GetVariable`/`SetVariable` blocks referencing that name.
2626

2727
- [ ] **No right-click context menu** — No context menu exists on the canvas, nodes, or links. The shared UI library has a `ContextMenu` primitive (`sharedUiComponents/src/fluent/primitives/contextMenu.tsx`) but the flow graph editor doesn't use it. Common operations require memorizing keyboard shortcuts. **Expected:** Right-click context menus for:
2828
- **Canvas:** Add block (opens search), Paste, Create sticky note, Create frame, Select all

packages/tools/flowGraphEditor/MANUAL.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,40 @@ The loaded scene's objects (meshes, lights, cameras, etc.) become available as r
4444
- **Load from file** — Import a previously saved JSON file.
4545
- **Load from snippet** — Enter a snippet ID to restore a graph (and its associated scene, if any).
4646

47+
### How to Use (Embed Code)
48+
49+
Click the **&lt;/&gt;** button in the toolbar to open the **How to Use** dialog. It shows copy-to-clipboard code samples for integrating your flow graph into a project:
50+
51+
- **From snippet server** — Fetch the snippet JSON from `https://snippet.babylonjs.com/<snippetId>`, parse the payload, then call `ParseFlowGraphAsync(data, { coordinator })` from `@babylonjs/core/FlowGraph/flowGraphParser`.
52+
- **From JSON file** — Load the saved `.json` file and call `ParseFlowGraphAsync(data, { coordinator })` from `@babylonjs/core/FlowGraph/flowGraphParser`.
53+
54+
Each code sample includes the necessary import statements and is ready to paste into your project.
55+
56+
---
57+
58+
## Variables Panel
59+
60+
The **Variables** section in the property panel (right sidebar, when no block is selected) lists all variables referenced by `GetVariable` and `SetVariable` blocks in the graph.
61+
62+
### Viewing Variables
63+
64+
Each variable row shows:
65+
66+
- The **variable name**
67+
- A reference count (`2G / 1S` means 2 GetVariable blocks and 1 SetVariable block reference it)
68+
69+
### Adding Variables
70+
71+
Click **+ Add** to create a new variable with an auto-generated name. The variable is registered on context 0 with an undefined default value.
72+
73+
### Renaming Variables
74+
75+
Double-click a variable name to edit it. Press **Enter** to confirm or **Escape** to cancel. Renaming propagates to all `GetVariable` and `SetVariable` blocks that reference the old name, and also updates the variable in all execution contexts.
76+
77+
### Deleting Variables
78+
79+
Click the **** button on a variable row to delete it. This removes all `GetVariable` and `SetVariable` blocks that reference the variable, and removes the variable from all execution contexts.
80+
4781
### glTF Import / Export
4882

4983
**Importing a glTF with an interactive flow graph:**

packages/tools/flowGraphEditor/src/components/graphControls/graphControlsComponent.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,15 @@ export class GraphControlsComponent extends React.Component<IGraphControlsProps,
397397
>
398398
?
399399
</button>
400+
<button
401+
className="fge-ctrl-btn fge-ctrl-howto"
402+
title="How to Use (embed code samples)"
403+
onClick={() => {
404+
this.props.globalState.onHowToUseRequested.notifyObservers();
405+
}}
406+
>
407+
{"</>"}
408+
</button>
400409
</div>
401410
);
402411
}

packages/tools/flowGraphEditor/src/components/help/helpContent.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type HelpTopicId =
1414
| "smart-groups"
1515
| "keyboard-shortcuts"
1616
| "block-properties"
17+
| "how-to-use"
18+
| "variables"
1719
| "gltf-import-export"
1820
| "composite-templates";
1921

@@ -354,6 +356,41 @@ export const HelpTopics: IHelpTopic[] = [
354356
},
355357
],
356358
},
359+
{
360+
id: "how-to-use",
361+
title: "How to Use (Embed Code)",
362+
sections: [
363+
{
364+
html: `<p>Click the <b>&lt;/&gt;</b> button in the toolbar to open the <b>How to Use</b> dialog. It shows ready-to-paste code samples for loading your flow graph in your own project.</p>`,
365+
},
366+
{
367+
heading: "From Snippet Server",
368+
html: `<p>If you've saved your graph to the snippet server, the dialog pre-fills the snippet ID. Fetch the snippet JSON and use <code>ParseFlowGraphAsync()</code> from <code>@babylonjs/core/FlowGraph/flowGraphParser</code> to load it.</p>`,
369+
},
370+
{
371+
heading: "From JSON File",
372+
html: `<p>Save your graph as a JSON file, then use <code>ParseFlowGraphAsync()</code> from <code>@babylonjs/core/FlowGraph/flowGraphParser</code> to parse it. Both methods require a <code>FlowGraphCoordinator</code> tied to your scene.</p>`,
373+
},
374+
],
375+
},
376+
{
377+
id: "variables",
378+
title: "Variables Panel",
379+
sections: [
380+
{
381+
html: `<p>The <b>Variables</b> section in the right property panel lists all variables referenced by <code>GetVariable</code> and <code>SetVariable</code> blocks.</p>`,
382+
},
383+
{
384+
heading: "Managing Variables",
385+
html: `<ul>
386+
<li><b>+ Add</b> — creates a new variable with an auto-generated name.</li>
387+
<li><b>Double-click</b> a name to rename. Renaming propagates to all Get/Set blocks.</li>
388+
<li><b>✕</b> — deletes the variable and removes all blocks referencing it.</li>
389+
</ul>
390+
<p>Each row shows a reference count (e.g., <code>2G / 1S</code> = 2 get blocks, 1 set block).</p>`,
391+
},
392+
],
393+
},
357394
{
358395
id: "gltf-import-export",
359396
title: "glTF Import / Export",
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
.fge-howto-overlay {
2+
position: fixed;
3+
top: 0;
4+
left: 0;
5+
width: 100%;
6+
height: 100%;
7+
background: rgba(0, 0, 0, 0.5);
8+
z-index: 9000;
9+
display: flex;
10+
align-items: center;
11+
justify-content: center;
12+
}
13+
14+
.fge-howto-dialog {
15+
background: #2a2a2a;
16+
border: 1px solid #555;
17+
border-radius: 8px;
18+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
19+
width: 600px;
20+
max-width: 90vw;
21+
max-height: 80vh;
22+
display: flex;
23+
flex-direction: column;
24+
font:
25+
14px "acumin-pro",
26+
sans-serif;
27+
color: #ccc;
28+
29+
.fge-howto-header {
30+
display: flex;
31+
align-items: center;
32+
justify-content: space-between;
33+
padding: 16px 20px 12px;
34+
border-bottom: 1px solid #444;
35+
36+
h2 {
37+
margin: 0;
38+
font-size: 16px;
39+
font-weight: 600;
40+
color: #eee;
41+
}
42+
43+
.fge-howto-close {
44+
background: none;
45+
border: none;
46+
color: #888;
47+
cursor: pointer;
48+
font-size: 18px;
49+
padding: 0;
50+
line-height: 1;
51+
&:hover {
52+
color: #fff;
53+
}
54+
}
55+
}
56+
57+
.fge-howto-body {
58+
padding: 16px 20px;
59+
overflow-y: auto;
60+
flex: 1;
61+
62+
.fge-howto-section {
63+
margin-bottom: 20px;
64+
65+
h3 {
66+
margin: 0 0 8px;
67+
font-size: 14px;
68+
font-weight: 600;
69+
color: #ddd;
70+
}
71+
72+
p {
73+
margin: 0 0 8px;
74+
font-size: 13px;
75+
line-height: 1.5;
76+
color: #aaa;
77+
}
78+
}
79+
80+
.fge-howto-code-block {
81+
position: relative;
82+
background: #1e1e1e;
83+
border: 1px solid #444;
84+
border-radius: 4px;
85+
padding: 12px 40px 12px 12px;
86+
font:
87+
12px "Consolas",
88+
"Courier New",
89+
monospace;
90+
color: #d4d4d4;
91+
white-space: pre-wrap;
92+
word-break: break-all;
93+
line-height: 1.5;
94+
overflow-x: auto;
95+
96+
.fge-howto-copy-btn {
97+
position: absolute;
98+
top: 6px;
99+
right: 6px;
100+
background: #464646;
101+
border: 1px solid #666;
102+
border-radius: 3px;
103+
color: #ccc;
104+
cursor: pointer;
105+
font-size: 11px;
106+
padding: 2px 8px;
107+
&:hover {
108+
background: #555;
109+
color: #fff;
110+
}
111+
&.copied {
112+
background: #2d7a3a;
113+
color: #fff;
114+
border-color: #2d7a3a;
115+
}
116+
}
117+
}
118+
}
119+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import * as React from "react";
2+
import { type GlobalState } from "../../globalState";
3+
import "./howToUse.scss";
4+
5+
interface IHowToUseDialogProps {
6+
globalState: GlobalState;
7+
onClose: () => void;
8+
}
9+
10+
interface IHowToUseDialogState {
11+
copiedIndex: number | null;
12+
}
13+
14+
/**
15+
* Dialog that shows code samples for integrating a saved flow graph into a user's project.
16+
*/
17+
export class HowToUseDialogComponent extends React.Component<IHowToUseDialogProps, IHowToUseDialogState> {
18+
/** @internal */
19+
constructor(props: IHowToUseDialogProps) {
20+
super(props);
21+
this.state = { copiedIndex: null };
22+
}
23+
24+
private _copyToClipboard(text: string, index: number) {
25+
if (navigator.clipboard) {
26+
navigator.clipboard
27+
.writeText(text)
28+
// eslint-disable-next-line github/no-then
29+
.then(() => {
30+
this.setState({ copiedIndex: index });
31+
setTimeout(() => this.setState({ copiedIndex: null }), 2000);
32+
})
33+
// eslint-disable-next-line github/no-then
34+
.catch(() => {
35+
/* clipboard not available */
36+
});
37+
}
38+
}
39+
40+
private _renderCodeBlock(code: string, index: number) {
41+
const isCopied = this.state.copiedIndex === index;
42+
return (
43+
<div className="fge-howto-code-block">
44+
<button className={`fge-howto-copy-btn${isCopied ? " copied" : ""}`} onClick={() => this._copyToClipboard(code, index)}>
45+
{isCopied ? "Copied!" : "Copy"}
46+
</button>
47+
{code}
48+
</div>
49+
);
50+
}
51+
52+
/** @internal */
53+
override render() {
54+
const snippetId = this.props.globalState.flowGraphSnippetId;
55+
56+
const snippetCode = `import { FlowGraphCoordinator } from "@babylonjs/core/FlowGraph/flowGraphCoordinator";
57+
import { ParseFlowGraphAsync } from "@babylonjs/core/FlowGraph/flowGraphParser";
58+
59+
// After creating your scene:
60+
const coordinator = new FlowGraphCoordinator({ scene });
61+
62+
// Fetch the snippet from the Babylon.js snippet server:
63+
const response = await fetch(
64+
"https://snippet.babylonjs.com/${snippetId || "<your-snippet-id>"}"
65+
);
66+
const snippet = await response.json();
67+
const data = JSON.parse(snippet.jsonPayload);
68+
69+
// Parse and start the flow graph:
70+
const flowGraph = await ParseFlowGraphAsync(
71+
JSON.parse(data.flowGraph),
72+
{ coordinator }
73+
);
74+
flowGraph.start();`;
75+
76+
const fileCode = `import { FlowGraphCoordinator } from "@babylonjs/core/FlowGraph/flowGraphCoordinator";
77+
import { ParseFlowGraphAsync } from "@babylonjs/core/FlowGraph/flowGraphParser";
78+
79+
// Load the saved JSON file:
80+
const response = await fetch("./flowGraph.json");
81+
const data = await response.json();
82+
83+
// After creating your scene:
84+
const coordinator = new FlowGraphCoordinator({ scene });
85+
const flowGraph = await ParseFlowGraphAsync(
86+
data,
87+
{ coordinator }
88+
);
89+
flowGraph.start();`;
90+
91+
return (
92+
<div className="fge-howto-overlay" onPointerDown={() => this.props.onClose()}>
93+
<div className="fge-howto-dialog" onPointerDown={(e) => e.stopPropagation()}>
94+
<div className="fge-howto-header">
95+
<h2>How to Use This Flow Graph</h2>
96+
<button className="fge-howto-close" aria-label="Close" onClick={this.props.onClose}>
97+
98+
</button>
99+
</div>
100+
<div className="fge-howto-body">
101+
<div className="fge-howto-section">
102+
<h3>Method 1: From Snippet Server</h3>
103+
<p>
104+
{snippetId
105+
? `Your graph is saved with snippet ID: ${snippetId}. Use the following code to load it.`
106+
: "Save your graph to the snippet server first, then use the snippet ID in the code below."}
107+
</p>
108+
{this._renderCodeBlock(snippetCode, 0)}
109+
</div>
110+
111+
<div className="fge-howto-section">
112+
<h3>Method 2: From JSON File</h3>
113+
<p>Download your graph as a JSON file (using the Save button in the property panel), then load it with this code.</p>
114+
{this._renderCodeBlock(fileCode, 1)}
115+
</div>
116+
117+
<div className="fge-howto-section">
118+
<h3>Notes</h3>
119+
<p>
120+
Both methods create a <code>FlowGraphCoordinator</code> that manages the execution context. Call <code>flowGraph.start()</code> after parsing to
121+
begin execution. The scene&apos;s objects (meshes, lights, cameras) will be automatically available to the flow graph through the coordinator&apos;s
122+
context.
123+
</p>
124+
</div>
125+
</div>
126+
</div>
127+
</div>
128+
);
129+
}
130+
}

0 commit comments

Comments
 (0)