arrangeConnectorLabels function

Map<String, List<ConnectorLabel>>? arrangeConnectorLabels(
  1. ResolvedScene scene,
  2. Set<String> connectorIds, {
  3. required double spacing,
})

Plans label positions from measured geometry without changing the scene. Returns null if the bounded candidate set cannot place every requested label. Container interiors are available; leaf shapes and other labels are obstacles.

Implementation

Map<String, List<ConnectorLabel>>? arrangeConnectorLabels(
  ResolvedScene scene,
  Set<String> connectorIds, {
  required double spacing,
}) {
  final occupied = <DiagramRect>[];
  final changes = <String, List<ConnectorLabel>>{};
  final connectors = scene.elements.whereType<ResolvedConnector>().toList()
    ..sort((a, b) => a.id.compareTo(b.id));
  for (final connector in connectors) {
    if (!connectorIds.contains(connector.id)) {
      occupied.addAll(connector.labels.map((label) => label.visualBounds));
    }
  }

  final clips = {
    for (final connector in connectors)
      if (connectorIds.contains(connector.id))
        connector.id: scene.clippingAncestorsOf(connector).toList(),
  };
  bool fits(ResolvedConnector connector, DiagramRect bounds) {
    final area = bounds.inflate(spacing);
    if (![
      area.left,
      area.top,
      area.right,
      area.bottom,
    ].every((v) => v.isFinite)) {
      return false;
    }
    if (occupied.any(area.intersects)) return false;
    for (final parent in clips[connector.id]!) {
      // A local envelope is conservative for rotated clips and uses the same
      // outline containment contract as other portable content layout.
      if (!parent.outline.containsBounds(
        DiagramRect.fromPoints(area.corners.map(parent.worldToLocal)),
      )) {
        return false;
      }
    }
    return !scene.spatialIndex
        .query(area)
        .any(
          (element) =>
              element is ResolvedFramedElement &&
              element.container == null &&
              area.intersects(element.bounds),
        );
  }

  // Reserve already valid labels first, so repairing one collision never
  // needlessly displaces a label elsewhere in the drawing.
  final retained = <(String, String)>{};
  for (final connector in connectors) {
    if (!connectorIds.contains(connector.id)) continue;
    for (final label in connector.labels) {
      final anchor = connector.pathGeometry.sample(label.source.fraction);
      final offset = label.source.offset;
      final distance =
          (anchor.normal.x.abs() * label.visualBounds.width +
                  anchor.normal.y.abs() * label.visualBounds.height) /
              2 +
          spacing;
      final along = offset.x * anchor.tangent.x + offset.y * anchor.tangent.y;
      final across = offset.x * anchor.normal.x + offset.y * anchor.normal.y;
      if (along.abs() <= spacing &&
          across.abs() <= distance + 32 + 1e-6 &&
          fits(connector, label.visualBounds)) {
        retained.add((connector.id, label.source.id));
        occupied.add(label.visualBounds);
      }
    }
  }
  for (final connector in connectors) {
    if (!connectorIds.contains(connector.id)) continue;
    final labels = <ConnectorLabel>[];
    for (final label in connector.labels) {
      if (retained.contains((connector.id, label.source.id))) {
        labels.add(label.source);
        continue;
      }
      ConnectorLabel? placed;
      final fractions = <double>{
        label.source.fraction,
        .5,
        .25,
        .75,
        .15,
        .85,
        for (var step = 1; step < 20; step++) step / 20,
      };
      search:
      for (final extraGap in [0.0, 32.0]) {
        for (final fraction in fractions) {
          final anchor = connector.pathGeometry.sample(fraction);
          final distance =
              (anchor.normal.x.abs() * label.visualBounds.width +
                      anchor.normal.y.abs() * label.visualBounds.height) /
                  2 +
              spacing +
              extraGap;
          final offsets = [
            if (extraGap == 0) DiagramPoint.zero,
            anchor.normal * -distance,
            anchor.normal * distance,
          ];
          for (final offset in offsets) {
            final center = anchor.position + offset;
            final bounds = label.visualBounds.translate(
              center - label.bounds.center,
            );
            if (!fits(connector, bounds)) continue;
            placed = label.source.copyWith(fraction: fraction, offset: offset);
            occupied.add(bounds);
            break search;
          }
        }
      }
      if (placed == null) return null;
      labels.add(placed);
    }
    if (List.generate(
      labels.length,
      (i) => labels[i] != connector.source.labels[i],
    ).any((changed) => changed)) {
      changes[connector.id] = List.unmodifiable(labels);
    }
  }
  return Map.unmodifiable(changes);
}