arrangeConnectorLabels function
Map<String, List<ConnectorLabel> > ?
arrangeConnectorLabels(
- ResolvedScene scene,
- Set<
String> connectorIds, { - 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);
}