toJson method
Convert to JSON map for ELK JS serialization.
Implementation
Map<String, dynamic> toJson() {
// Build a local resolver: nodeId → (portIndex → portId string).
String resolvePort(String nodeId, int portIndex) {
if (nodeId == id) {
return portIdAt(portIndex);
}
for (final child in children) {
if (child.id == nodeId) {
return child.portIdAt(portIndex);
}
}
if (hiddenChildren != null) {
for (final child in hiddenChildren!) {
if (child.id == nodeId) {
return child.portIdAt(portIndex);
}
}
}
return portIndex.toString(); // fallback
}
// Build edge JSON from hyperedges on-the-fly (expansion + visibility).
final visibleEdgeJson = <Map<String, dynamic>>[];
final hiddenEdgeJson = <Map<String, dynamic>>[];
// Self-referencing edges (port-to-port passthrough) that need dummy
// split nodes for ELK routing. Deduplicated by (srcPort, tgtPort).
final selfEdgesByPortPair = <String, Map<String, dynamic>>{};
if (hyperedges != null) {
final visibleIds = <String>{id};
for (final child in children) {
visibleIds.add(child.id);
}
final allowedIds = isPartiallyExpanded ? partialHyperedgeIds : null;
for (final h in hyperedges!) {
final allowed = allowedIds == null || allowedIds.contains(h.id);
for (final edgeMap in h.toElkEdges(resolvePort)) {
final srcNode = edgeMap['source'] as String;
final tgtNode = edgeMap['target'] as String;
final isSelfEdge = srcNode == id && tgtNode == id;
if (isSelfEdge) {
if (allowed && children.isNotEmpty) {
// Deduplicate by port pair — multi-bit nets produce identical
// self-edges that should be a single routed wire.
final key = '${edgeMap['sourcePort']}|${edgeMap['targetPort']}';
selfEdgesByPortPair.putIfAbsent(key, () => edgeMap);
} else {
hiddenEdgeJson.add(edgeMap);
}
continue;
}
final bothVisible =
visibleIds.contains(srcNode) && visibleIds.contains(tgtNode);
if (allowed && bothVisible) {
visibleEdgeJson.add(edgeMap);
} else {
hiddenEdgeJson.add(edgeMap);
}
}
}
}
// Split each unique self-edge through a zero-size dummy child so ELK
// routes input→dummy and dummy→output inside the compound node.
final dummyChildren = <Map<String, dynamic>>[];
var dummyIdx = 0;
for (final edge in selfEdgesByPortPair.values) {
final edgeId = edge['id'] as String;
final dummyId = '${id}_pt$dummyIdx';
final dummyInPort = '$dummyId:0';
final dummyOutPort = '$dummyId:1';
dummyIdx++;
dummyChildren.add({
'id': dummyId,
'hwMeta': {'name': '', '_passthrough': true},
'properties': {'org.eclipse.elk.portConstraints': 'FIXED_ORDER'},
'ports': [
{
'id': dummyInPort,
'hwMeta': {'name': ''},
'direction': 'INPUT',
'properties': {'side': 'WEST', 'index': 0},
},
{
'id': dummyOutPort,
'hwMeta': {'name': ''},
'direction': 'OUTPUT',
'properties': {'side': 'EAST', 'index': 1},
},
],
'width': 0,
'height': 0,
});
visibleEdgeJson
..add({
'id': '${edgeId}_a',
'source': id,
'sourcePort': edge['sourcePort'],
'target': dummyId,
'targetPort': dummyInPort,
'hwMeta': edge['hwMeta'],
'_passthroughGroup': edgeId,
})
..add({
'id': '${edgeId}_b',
'source': dummyId,
'sourcePort': dummyOutPort,
'target': id,
'targetPort': edge['targetPort'],
'hwMeta': edge['hwMeta'],
'_passthroughGroup': edgeId,
});
}
// Assemble children list including dummy passthrough nodes.
final childrenJson = children.map((c) => c.toJson()).toList()
..addAll(dummyChildren);
return {
'id': id,
'hwMeta': hwMeta.toJson(),
'properties': properties,
if (hierarchyNodeId != null) 'hierarchyNodeId': hierarchyNodeId,
if (elkPorts.isNotEmpty)
'ports': elkPorts.map((p) => p.toJson()).toList(),
if (childrenJson.isNotEmpty) 'children': childrenJson,
if (hiddenChildren != null && hiddenChildren!.isNotEmpty)
'_children': hiddenChildren!.map((c) => c.toJson()).toList(),
if (visibleEdgeJson.isNotEmpty) 'edges': visibleEdgeJson,
if (hiddenEdgeJson.isNotEmpty) '_edges': hiddenEdgeJson,
if (isPartiallyExpanded) 'isPartiallyExpanded': true,
if (slimConnectedPortIds != null && slimConnectedPortIds!.isNotEmpty)
'_connectedPorts': slimConnectedPortIds!.toList(),
if (x != null) 'x': x,
if (y != null) 'y': y,
if (width != null) 'width': width,
if (height != null) 'height': height,
if (padding != null) 'padding': padding!.toJson(),
};
}