ModulesData structures3D Configurator
Out of the box, the editor can handle any JS objects as nodes and connections, as long as they include a mandatory id
field for both. Additionally, connections must have source
and target
fields. These objects can be either plain data objects or objects that include methods. Check out the Data structures guide for details.
The current version of the editor doesn't support importing/exporting by default due to some limitations:
Suppose we have a straightforward example where the node is a valid JSON object
import { NodeEditor, BaseSchemes, getUID } from 'rete'
const editor = new NodeEditor<BaseSchemes>()
const node = { id: getUID(), label: 'Label' }
await editor.addNode(node)
In this case, we can easily export such nodes into JSON to save them in a database, for instance.
Objects that aren't valid JSON, such as instances of classes, objects with functions, or objects with cyclic references pose a challenge. Discarding them would mean losing the advantages that come with using JS.
For instance, node classes can be used to create nodes, allowing for more reliable and convenient interaction through methods, and providing the flexibility to use different paradigms.
import { ClassicPreset } from 'rete'
const node = new ClassicPreset.Node('Label')
node.addOutput('port', new ClassicPreset.Output(socket, 'Label'))
await editor.addNode(node)
While serialization and deserialization might be one way to convert such objects into valid JSON, this approach may not work in complex scenarios.
If you want to export a graph, you can use the following code as a reference. Please note that this is not a fully functional code, but rather an approximation to help you implement your own import/export functionality according to your specific requirements.
const data = { nodes: [] }
const nodes = editor.getNodes()
for (const node of nodes) {
data.nodes.push({
id: node.id,
label: node.label,
inputs: /// ....
controls: /// ....
outputs: /// ....
})
}
In order to revert the transformation, we must initialize node instances, inputs, outputs, and controls based on the objects provided.
for (const { id, label, inputs, outputs, controls } of data.nodes) {
const node = new ClassicPreset.Node(label);
node.id = id;
/// ... inputs
/// ... controls
/// ... outputs
await editor.addNode(node)
}
The complete example can be found at the link. Note that this example has been simplified for ease of understanding.
Additionally, importing or exporting inputs and outputs may not always be necessary if they are static and we know the node type. In such cases, we can simply save the node name and relevant data, which can be used to create instances of nodes with pre-defined ports. Check out the 3D Configurator and the Modules examples with implementation of this approach.
Another challenge during graph import could be the order in which nodes need to be imported. In a simple cases, the order will be the same as the order in which they were added to the editor.
const graph = /// loaded JSON-valid object from DB
for (const node of graph.nodes) {
await editor.addNode(node)
}
When dealing with more complex graphs, the order of adding nodes can vary. For instance, in a graph with nested nodes, it might be necessary to add parent nodes prior to child nodes. Moreover, it's quite possible that a user may create a child node before its parent node while working in the editor.
Let's take a look at an example of importing a graph where certain nodes have parent
field indicating their association with another node. As a result, these nodes must be imported after their parent node has been created.
async function importForParent(nodes, parent = undefined) {
const nodes = nodes.filter(node => node.parent === parent)
for (const node of nodes) {
await editor.addNode(node)
await importForParent(nodes, node.id)
}
}
const graph = /// loaded JSON-valid object from DB
await importForParent(graph.nodes)
Since this approach is more complex and there are multiple ways to do it, the import method will vary depending on your specific use case.