Skip to content

Commit

Permalink
account for node configuration in graph editor
Browse files Browse the repository at this point in the history
The objective of this commit is to correctly render graphs like these:
https://github.com/bhouston/behave-graph/blob/3e6568caa9a46a6f6aee27b0192a478f30c9c541/graphs/core/flow/WaitAll.json

When loading JSON, a node may have arbitrary configuration parameters,
that previously got ignored (such as number of sequence outputs).

This commit tracks configuration for each rendered node,
and accounts for it in all calculations (such as adding new edge).

In order to do this, instead of NodeSpecJSON (object) we now pass
a NodeSpecGenerator (class) which generates node specs on demand
based on passed configuration (and also memoizes, not sure if that's
necessary).

New nodes will still be generated with no configuration,
and no UI work to support it has been done.
  • Loading branch information
rlidwka committed Oct 4, 2023
1 parent 3e6568c commit 8c6deaa
Show file tree
Hide file tree
Showing 23 changed files with 232 additions and 165 deletions.
4 changes: 2 additions & 2 deletions apps/export-node-spec/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ManualLifecycleEventEmitter,
registerCoreProfile,
validateNodeRegistry,
writeNodeSpecsToJSON
writeDefaultNodeSpecsToJSON,
} from '@behave-graph/core';
import { DummyScene, registerSceneProfile } from '@behave-graph/scene';
import { program } from 'commander';
Expand Down Expand Up @@ -59,7 +59,7 @@ export const main = async () => {
return;
}

const nodeSpecJson = writeNodeSpecsToJSON(registry);
const nodeSpecJson = writeDefaultNodeSpecsToJSON(registry);
nodeSpecJson.sort((a, b) => a.type.localeCompare(b.type));
const jsonOutput = JSON.stringify(nodeSpecJson, null, ' ');
if (programOptions.csv) {
Expand Down
98 changes: 52 additions & 46 deletions packages/core/src/Graphs/IO/writeNodeSpecsToJSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NodeCategory } from '../../Nodes/NodeDefinitions.js';
import { IRegistry } from '../../Registry.js';
import { Choices } from '../../Sockets/Socket.js';
import { createNode, makeGraphApi } from '../Graph.js';
import { NodeConfigurationJSON } from './GraphJSON.js';
import {
ChoiceJSON,
InputSocketSpecJSON,
Expand All @@ -16,64 +17,69 @@ function toChoices(valueChoices: Choices | undefined): ChoiceJSON | undefined {
});
}

export function writeNodeSpecsToJSON(registry: IRegistry): NodeSpecJSON[] {
const nodeSpecsJSON: NodeSpecJSON[] = [];

// const graph = new Graph(registry);

// create JSON specs for a single node based on given configuration
export function writeNodeSpecToJSON(registry: IRegistry, nodeTypeName: string, configuration: NodeConfigurationJSON): NodeSpecJSON {
const graph = makeGraphApi({
...registry,
customEvents: {},
variables: {}
});

Object.keys(registry.nodes).forEach((nodeTypeName) => {
const node = createNode({
graph,
registry,
nodeTypeName
});
const node = createNode({
graph,
registry,
nodeTypeName,
nodeConfiguration: configuration,
});

const nodeSpecJSON: NodeSpecJSON = {
type: nodeTypeName,
category: node.description.category as NodeCategory,
label: node.description.label,
inputs: [],
outputs: [],
configuration: []
};

node.inputs.forEach((inputSocket) => {
const valueType =
inputSocket.valueTypeName === 'flow'
? undefined
: registry.values[inputSocket.valueTypeName];

const nodeSpecJSON: NodeSpecJSON = {
type: nodeTypeName,
category: node.description.category as NodeCategory,
label: node.description.label,
inputs: [],
outputs: [],
configuration: []
let defaultValue = inputSocket.value;
if (valueType !== undefined) {
defaultValue = valueType.serialize(defaultValue);
}
if (defaultValue === undefined && valueType !== undefined) {
defaultValue = valueType.serialize(valueType.creator());
}
const socketSpecJSON: InputSocketSpecJSON = {
name: inputSocket.name,
valueType: inputSocket.valueTypeName,
defaultValue,
choices: toChoices(inputSocket.valueChoices)
};
nodeSpecJSON.inputs.push(socketSpecJSON);
});

node.inputs.forEach((inputSocket) => {
const valueType =
inputSocket.valueTypeName === 'flow'
? undefined
: registry.values[inputSocket.valueTypeName];
node.outputs.forEach((outputSocket) => {
const socketSpecJSON: OutputSocketSpecJSON = {
name: outputSocket.name,
valueType: outputSocket.valueTypeName
};
nodeSpecJSON.outputs.push(socketSpecJSON);
});

let defaultValue = inputSocket.value;
if (valueType !== undefined) {
defaultValue = valueType.serialize(defaultValue);
}
if (defaultValue === undefined && valueType !== undefined) {
defaultValue = valueType.serialize(valueType.creator());
}
const socketSpecJSON: InputSocketSpecJSON = {
name: inputSocket.name,
valueType: inputSocket.valueTypeName,
defaultValue,
choices: toChoices(inputSocket.valueChoices)
};
nodeSpecJSON.inputs.push(socketSpecJSON);
});
return nodeSpecJSON;
}

node.outputs.forEach((outputSocket) => {
const socketSpecJSON: OutputSocketSpecJSON = {
name: outputSocket.name,
valueType: outputSocket.valueTypeName
};
nodeSpecJSON.outputs.push(socketSpecJSON);
});
// create JSON specs for all nodes with empty configuration
export function writeDefaultNodeSpecsToJSON(registry: IRegistry): NodeSpecJSON[] {
const nodeSpecsJSON: NodeSpecJSON[] = [];

nodeSpecsJSON.push(nodeSpecJSON);
Object.keys(registry.nodes).forEach((nodeTypeName) => {
nodeSpecsJSON.push(writeNodeSpecToJSON(registry, nodeTypeName, {}));
});

return nodeSpecsJSON;
Expand Down
13 changes: 7 additions & 6 deletions packages/flow/src/components/Controls.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GraphJSON, NodeSpecJSON } from '@behave-graph/core';
import { GraphJSON } from '@behave-graph/core';
import {
faDownload,
faPause,
Expand All @@ -16,27 +16,28 @@ import { ClearModal } from './modals/ClearModal.js';
import { HelpModal } from './modals/HelpModal.js';
import { Examples, LoadModal } from './modals/LoadModal.js';
import { SaveModal } from './modals/SaveModal.js';
import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js';

export type CustomControlsProps = {
playing: boolean;
togglePlay: () => void;
setBehaviorGraph: (value: GraphJSON) => void;
examples: Examples;
specJson: NodeSpecJSON[] | undefined;
specGenerator: NodeSpecGenerator | undefined;
};

export const CustomControls: React.FC<CustomControlsProps> = ({
playing,
togglePlay,
setBehaviorGraph,
examples,
specJson
specGenerator
}: {
playing: boolean;
togglePlay: () => void;
setBehaviorGraph: (value: GraphJSON) => void;
examples: Examples;
specJson: NodeSpecJSON[] | undefined;
specGenerator: NodeSpecGenerator | undefined;
}) => {
const [loadModalOpen, setLoadModalOpen] = useState(false);
const [saveModalOpen, setSaveModalOpen] = useState(false);
Expand Down Expand Up @@ -68,10 +69,10 @@ export const CustomControls: React.FC<CustomControlsProps> = ({
setBehaviorGraph={setBehaviorGraph}
examples={examples}
/>
{specJson && (
{specGenerator && (
<SaveModal
open={saveModalOpen}
specJson={specJson}
specGenerator={specGenerator}
onClose={() => setSaveModalOpen(false)}
/>
)}
Expand Down
12 changes: 6 additions & 6 deletions packages/flow/src/components/Flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Background, BackgroundVariant, ReactFlow } from 'reactflow';
import { useBehaveGraphFlow } from '../hooks/useBehaveGraphFlow.js';
import { useFlowHandlers } from '../hooks/useFlowHandlers.js';
import { useGraphRunner } from '../hooks/useGraphRunner.js';
import { useNodeSpecJson } from '../hooks/useNodeSpecJson.js';
import { useNodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js';
import CustomControls from './Controls.js';
import { Examples } from './modals/LoadModal.js';
import { NodePicker } from './NodePicker.js';
Expand All @@ -21,7 +21,7 @@ export const Flow: React.FC<FlowProps> = ({
registry,
examples
}) => {
const specJson = useNodeSpecJson(registry);
const specGenerator = useNodeSpecGenerator(registry);

const {
nodes,
Expand All @@ -33,7 +33,7 @@ export const Flow: React.FC<FlowProps> = ({
nodeTypes
} = useBehaveGraphFlow({
initialGraphJson: graph,
specJson
specGenerator
});

const {
Expand All @@ -51,7 +51,7 @@ export const Flow: React.FC<FlowProps> = ({
nodes,
onEdgesChange,
onNodesChange,
specJSON: specJson
specGenerator,
});

const { togglePlay, playing } = useGraphRunner({
Expand Down Expand Up @@ -81,7 +81,7 @@ export const Flow: React.FC<FlowProps> = ({
togglePlay={togglePlay}
setBehaviorGraph={setGraphJson}
examples={examples}
specJson={specJson}
specGenerator={specGenerator}
/>
<Background
variant={BackgroundVariant.Lines}
Expand All @@ -94,7 +94,7 @@ export const Flow: React.FC<FlowProps> = ({
filters={nodePickFilters}
onPickNode={handleAddNode}
onClose={closeNodePicker}
specJSON={specJson}
specJSON={specGenerator?.getAllNodeSpecs()}
/>
)}
</ReactFlow>
Expand Down
9 changes: 5 additions & 4 deletions packages/flow/src/components/InputSocket.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InputSocketSpecJSON, NodeSpecJSON } from '@behave-graph/core';
import { InputSocketSpecJSON } from '@behave-graph/core';
import { faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import cx from 'classnames';
Expand All @@ -8,12 +8,13 @@ import { Connection, Handle, Position, useReactFlow } from 'reactflow';
import { colors, valueTypeColorMap } from '../util/colors.js';
import { isValidConnection } from '../util/isValidConnection.js';
import { AutoSizeInput } from './AutoSizeInput.js';
import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js';

export type InputSocketProps = {
connected: boolean;
value: any | undefined;
onChange: (key: string, value: any) => void;
specJSON: NodeSpecJSON[];
specGenerator: NodeSpecGenerator;
} & InputSocketSpecJSON;

const InputFieldForValue = ({
Expand Down Expand Up @@ -95,7 +96,7 @@ const InputFieldForValue = ({

const InputSocket: React.FC<InputSocketProps> = ({
connected,
specJSON,
specGenerator,
...rest
}) => {
const { value, name, valueType, defaultValue, choices } = rest;
Expand Down Expand Up @@ -128,7 +129,7 @@ const InputSocket: React.FC<InputSocketProps> = ({
position={Position.Left}
className={cx(borderColor, connected ? backgroundColor : 'bg-gray-800')}
isValidConnection={(connection: Connection) =>
isValidConnection(connection, instance, specJSON)
isValidConnection(connection, instance, specGenerator)
}
/>
</div>
Expand Down
11 changes: 6 additions & 5 deletions packages/flow/src/components/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { isHandleConnected } from '../util/isHandleConnected.js';
import InputSocket from './InputSocket.js';
import NodeContainer from './NodeContainer.js';
import OutputSocket from './OutputSocket.js';
import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js';

type NodeProps = FlowNodeProps & {
spec: NodeSpecJSON;
allSpecs: NodeSpecJSON[];
specGenerator: NodeSpecGenerator;
};

const getPairs = <T, U>(arr1: T[], arr2: U[]) => {
Expand All @@ -28,7 +29,7 @@ export const Node: React.FC<NodeProps> = ({
data,
spec,
selected,
allSpecs
specGenerator,
}: NodeProps) => {
const edges = useEdges();
const handleChange = useChangeNodeData(id);
Expand All @@ -48,16 +49,16 @@ export const Node: React.FC<NodeProps> = ({
{input && (
<InputSocket
{...input}
specJSON={allSpecs}
value={data[input.name] ?? input.defaultValue}
specGenerator={specGenerator}
value={data.values[input.name] ?? input.defaultValue}
onChange={handleChange}
connected={isHandleConnected(edges, id, input.name, 'target')}
/>
)}
{output && (
<OutputSocket
{...output}
specJSON={allSpecs}
specGenerator={specGenerator}
connected={isHandleConnected(edges, id, output.name, 'source')}
/>
)}
Expand Down
9 changes: 5 additions & 4 deletions packages/flow/src/components/OutputSocket.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NodeSpecJSON, OutputSocketSpecJSON } from '@behave-graph/core';
import { OutputSocketSpecJSON } from '@behave-graph/core';
import { faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import cx from 'classnames';
Expand All @@ -7,14 +7,15 @@ import { Connection, Handle, Position, useReactFlow } from 'reactflow';

import { colors, valueTypeColorMap } from '../util/colors.js';
import { isValidConnection } from '../util/isValidConnection.js';
import { NodeSpecGenerator } from '../hooks/useNodeSpecGenerator.js';

export type OutputSocketProps = {
connected: boolean;
specJSON: NodeSpecJSON[];
specGenerator: NodeSpecGenerator;
} & OutputSocketSpecJSON;

export default function OutputSocket({
specJSON,
specGenerator,
connected,
valueType,
name
Expand Down Expand Up @@ -47,7 +48,7 @@ export default function OutputSocket({
position={Position.Right}
className={cx(borderColor, connected ? backgroundColor : 'bg-gray-800')}
isValidConnection={(connection: Connection) =>
isValidConnection(connection, instance, specJSON)
isValidConnection(connection, instance, specGenerator)
}
/>
</div>
Expand Down
9 changes: 5 additions & 4 deletions packages/flow/src/components/modals/SaveModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import { useEdges, useNodes } from 'reactflow';

import { flowToBehave } from '../../transformers/flowToBehave.js';
import { Modal } from './Modal.js';
import { NodeSpecGenerator } from '../../hooks/useNodeSpecGenerator.js';

export type SaveModalProps = {
open?: boolean;
onClose: () => void;
specJson: NodeSpecJSON[];
specGenerator: NodeSpecGenerator;
};

export const SaveModal: React.FC<SaveModalProps> = ({
open = false,
onClose,
specJson
specGenerator
}) => {
const ref = useRef<HTMLTextAreaElement>(null);
const [copied, setCopied] = useState(false);
Expand All @@ -24,8 +25,8 @@ export const SaveModal: React.FC<SaveModalProps> = ({
const nodes = useNodes();

const flow = useMemo(
() => flowToBehave(nodes, edges, specJson),
[nodes, edges, specJson]
() => flowToBehave(nodes, edges, specGenerator),
[nodes, edges, specGenerator]
);

const jsonString = JSON.stringify(flow, null, 2);
Expand Down

0 comments on commit 8c6deaa

Please sign in to comment.