Re-lay out diagrams to give crossed edge labels room

This commit is contained in:
Ian Gulliver
2026-10-23 11:10:52 -07:00
parent 83a734e9e6
commit 46039ae757
+33 -21
View File
@@ -452,7 +452,7 @@ private func headerSize(_ n: BoxNode) -> CGSize {
private struct Sizer {
let pairGaps: [String: CGFloat]
init(_ rows: [BoxRow], _ edges: [BoxEdge]) {
init(_ rows: [BoxRow], _ edges: [BoxEdge], _ extra: [Int: CGFloat]) {
var parents: [String: String] = [:]
func walk(_ n: BoxNode) {
for c in n.children {
@@ -469,8 +469,8 @@ private struct Sizer {
return out
}
var gaps: [String: CGFloat] = [:]
for e in edges {
let need = e.label.map { ceil(textSize($0, edgeLabelFont).width) + labelRoom } ?? edgeGap
for (i, e) in edges.enumerated() {
let need = (e.label.map { ceil(textSize($0, edgeLabelFont).width) + labelRoom } ?? edgeGap) + (extra[i] ?? 0)
for a in chain(e.from) {
for b in chain(e.to) where a != b && parents[a] == parents[b] {
let key = Sizer.pair(a, b)
@@ -598,16 +598,23 @@ private func projectEdges(_ d: BoxDiagram, _ level: String) -> (edges: [BoxEdge]
@MainActor
private func layoutLevel(_ d: BoxDiagram, _ level: String, _ done: [String: BoxLayout]) throws(DiagramError) -> BoxLayout {
let (edges, ghosts) = projectEdges(d, level)
var (boxes, size) = placeRows(d.levels[level]!, edges)
if !level.isEmpty {
(boxes, size) = framed(d, level, boxes, size, ghosts, edges, done)
var extra: [Int: CGFloat] = [:]
while true {
var (boxes, size) = placeRows(d.levels[level]!, edges, extra)
if !level.isEmpty {
(boxes, size) = framed(d, level, boxes, size, ghosts, edges, done)
}
let visible = Set(boxes.filter { $0.kind != .frame }.map(\.node.id))
let presets = d.presets.compactMap { p -> BoxPreset? in
let ids = Set(p.ids.map { d.end($0, at: level).id }).intersection(visible)
return ids.contains { d.rep($0, at: level) == $0 } ? BoxPreset(name: p.name, ids: ids) : nil
}
let (layout, stuck) = try routeLevel(level, boxes, size, edges, presets)
if stuck.isEmpty || !extra.isEmpty {
return layout
}
extra = stuck
}
let visible = Set(boxes.filter { $0.kind != .frame }.map(\.node.id))
let presets = d.presets.compactMap { p -> BoxPreset? in
let ids = Set(p.ids.map { d.end($0, at: level).id }).intersection(visible)
return ids.contains { d.rep($0, at: level) == $0 } ? BoxPreset(name: p.name, ids: ids) : nil
}
return try routeLevel(level, boxes, size, edges, presets)
}
@MainActor
@@ -666,8 +673,8 @@ private func framed(_ d: BoxDiagram, _ level: String, _ inner: [PlacedBox], _ in
}
@MainActor
private func placeRows(_ rows: [BoxRow], _ edges: [BoxEdge]) -> (boxes: [PlacedBox], size: CGSize) {
let sizer = Sizer(rows, edges)
private func placeRows(_ rows: [BoxRow], _ edges: [BoxEdge], _ extra: [Int: CGFloat]) -> (boxes: [PlacedBox], size: CGSize) {
let sizer = Sizer(rows, edges, extra)
let mainWidth: CGFloat = rows.map(sizer.rowWidth).max() ?? 0
let asideWidth: CGFloat = rows.flatMap(\.aside).map { sizer.naturalSize($0).width }.max() ?? 0
func ids(_ n: BoxNode) -> [String] {
@@ -684,13 +691,13 @@ private func placeRows(_ rows: [BoxRow], _ edges: [BoxEdge]) -> (boxes: [PlacedB
}
}
func gap(below i: Int) -> CGFloat {
let n = edges.filter { e in
guard let a = rowOf[e.from], let b = rowOf[e.to] else {
let crossing = edges.indices.filter { k in
guard let a = rowOf[edges[k].from], let b = rowOf[edges[k].to] else {
return false
}
return min(a, b) <= i && max(a, b) > i
}.count
return max(rowGap, CGFloat(n + 1) * laneGap)
}
return max(rowGap, CGFloat(crossing.count + 1) * laneGap) + crossing.compactMap { extra[$0] }.reduce(0, max)
}
var boxes: [PlacedBox] = []
var y = margin
@@ -719,7 +726,7 @@ private func placeRows(_ rows: [BoxRow], _ edges: [BoxEdge]) -> (boxes: [PlacedB
}
@MainActor
private func routeLevel(_ level: String, _ boxes: [PlacedBox], _ size: CGSize, _ edges: [BoxEdge], _ presets: [BoxPreset]) throws(DiagramError) -> BoxLayout {
private func routeLevel(_ level: String, _ boxes: [PlacedBox], _ size: CGSize, _ edges: [BoxEdge], _ presets: [BoxPreset]) throws(DiagramError) -> (BoxLayout, [Int: CGFloat]) {
let index = Dictionary(uniqueKeysWithValues: boxes.enumerated().map { ($0.element.node.id, $0.offset) })
var router = Router(boxes: boxes, index: index, size: size)
let order = edges.indices.sorted { a, b in
@@ -739,12 +746,17 @@ private func routeLevel(_ level: String, _ boxes: [PlacedBox], _ size: CGSize, _
var labels: [CGRect] = []
let lines = points.map { pts in zip(pts, pts.dropFirst()).map { a, b in CGRect(x: min(a.x, b.x), y: min(a.y, b.y), width: abs(a.x - b.x), height: abs(a.y - b.y)).insetBy(dx: -1, dy: -1) } }
var routed: [RoutedEdge] = []
for (e, pts) in zip(edges, points) {
var stuck: [Int: CGFloat] = [:]
for (i, (e, pts)) in zip(edges, points).enumerated() {
var labelRect: CGRect?
if let label = e.label {
let r = labelSpot(textSize(label, edgeLabelFont), pts, headers: headers, outlines: outlines, labels: labels, lines: lines.flatMap { $0 }, CGRect(x: 0, y: 0, width: size.width + margin, height: size.height + margin))
labels.append(r)
labelRect = r
let crossed = lines.indices.filter { $0 != i && lines[$0].contains { $0.intersects(r) } }.count
if crossed > 0 {
stuck[i] = CGFloat(crossed) * laneGap
}
}
routed.append(RoutedEdge(edge: e, points: pts, labelRect: labelRect))
}
@@ -754,7 +766,7 @@ private func routeLevel(_ level: String, _ boxes: [PlacedBox], _ size: CGSize, _
let shifted = routed.map { r in
RoutedEdge(edge: r.edge, points: r.points.map { CGPoint(x: $0.x + dx, y: $0.y + dy) }, labelRect: r.labelRect?.offsetBy(dx: dx, dy: dy))
}
return BoxLayout(level: level, boxes: boxes.map { $0.moved(dx, dy) }, index: index, edges: shifted, presets: presets, size: fit)
return (BoxLayout(level: level, boxes: boxes.map { $0.moved(dx, dy) }, index: index, edges: shifted, presets: presets, size: fit), stuck)
}
private struct Anchor {