toJson method

Map<String, dynamic> toJson()

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(),
  };
}