From 46039ae757e65fc992b5b66189883d53b196b37e Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 23 Oct 2026 11:10:52 -0700 Subject: [PATCH] Re-lay out diagrams to give crossed edge labels room --- mac/Sources/Byre/BoxDiagram.swift | 54 +++++++++++++++++++------------ 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/mac/Sources/Byre/BoxDiagram.swift b/mac/Sources/Byre/BoxDiagram.swift index 065e980..eacbf1c 100644 --- a/mac/Sources/Byre/BoxDiagram.swift +++ b/mac/Sources/Byre/BoxDiagram.swift @@ -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 {