From af9c7502984661b27d0ca68dc48147f5c3d68c06 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 21 Oct 2026 17:50:14 -0700 Subject: [PATCH] Keep diagram arrow labels clear and route arrows around box headers --- mac/Sources/Byre/BoxDiagram.swift | 52 +++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/mac/Sources/Byre/BoxDiagram.swift b/mac/Sources/Byre/BoxDiagram.swift index 90c09b7..e1d8a23 100644 --- a/mac/Sources/Byre/BoxDiagram.swift +++ b/mac/Sources/Byre/BoxDiagram.swift @@ -374,7 +374,12 @@ func layoutBoxDiagram(_ d: BoxDiagram) throws(DiagramError) -> BoxLayout { let sizer = Sizer(d) let mainWidth: CGFloat = d.rows.map(sizer.rowWidth).max() ?? 0 let asideWidth: CGFloat = d.rows.flatMap(\.aside).map { sizer.naturalSize($0).width }.max() ?? 0 - let asideX = margin + mainWidth + columnGap + func ids(_ n: BoxNode) -> [String] { + [n.id] + n.children.flatMap(ids) + } + let asideIDs = Set(d.rows.flatMap(\.aside).flatMap(ids)) + let crossing = d.edges.filter { asideIDs.contains($0.from) != asideIDs.contains($0.to) }.compactMap(\.label) + let asideX = margin + mainWidth + crossing.map { textSize($0, edgeLabelFont).width + 16 }.reduce(columnGap, max) var boxes: [PlacedBox] = [] var y = margin var asideBottom = margin - rowGap @@ -408,15 +413,17 @@ func layoutBoxDiagram(_ d: BoxDiagram) throws(DiagramError) -> BoxLayout { for i in order { points[i] = try router.route(d.edges[i]) } - var solid: [CGRect] = boxes.map(\.header) + let headers = boxes.map(\.header) + let outlines = boxes.map(\.rect) + 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 (i, (e, pts)) in zip(d.edges, points).enumerated() { var labelRect: CGRect? if let label = e.label { let others = lines.enumerated().filter { $0.offset != i }.flatMap(\.element) - let r = labelSpot(textSize(label, edgeLabelFont), pts, solid: solid, lines: others, CGRect(origin: .zero, size: size)) - solid.append(r) + let r = labelSpot(textSize(label, edgeLabelFont), pts, headers: headers, outlines: outlines, labels: labels, lines: others, CGRect(origin: .zero, size: size)) + labels.append(r) labelRect = r } routed.append(RoutedEdge(edge: e, points: pts, labelRect: labelRect)) @@ -424,7 +431,7 @@ func layoutBoxDiagram(_ d: BoxDiagram) throws(DiagramError) -> BoxLayout { return BoxLayout(boxes: boxes, index: index, edges: routed, presets: d.presets, size: size) } -private func labelSpot(_ size: CGSize, _ pts: [CGPoint], solid: [CGRect], lines: [CGRect], _ bounds: CGRect) -> CGRect { +private func labelSpot(_ size: CGSize, _ pts: [CGPoint], headers: [CGRect], outlines: [CGRect], labels: [CGRect], lines: [CGRect], _ bounds: CGRect) -> CGRect { var candidates: [(rect: CGRect, order: Int)] = [] let segs = zip(pts, pts.dropFirst()).sorted { a, b in abs(a.0.x - a.1.x) + abs(a.0.y - a.1.y) > abs(b.0.x - b.1.x) + abs(b.0.y - b.1.y) @@ -441,14 +448,23 @@ private func labelSpot(_ size: CGSize, _ pts: [CGPoint], solid: [CGRect], lines: } } } - func area(_ a: CGRect, _ b: CGRect) -> CGFloat { - let i = a.insetBy(dx: -2, dy: -2).intersection(b) + func area(_ a: CGRect, _ b: CGRect, _ clearance: CGFloat = 2) -> CGFloat { + let i = a.insetBy(dx: -clearance, dy: -clearance).intersection(b) return i.isNull ? 0 : max(1, i.width * i.height) } + func straddles(_ box: CGRect, _ c: CGRect) -> CGFloat { + let grown = c.insetBy(dx: -2, dy: -2) + if !grown.intersects(box) || box.contains(grown) { + return 0 + } + return area(box, c) + } func overlap(_ c: CGRect) -> CGFloat { let inside = c.intersection(bounds) let outside = c.width * c.height - (inside.isNull ? 0 : inside.width * inside.height) - let covered = solid.reduce(outside) { $0 + area($1, c) } + var covered = headers.reduce(outside) { $0 + area($1, c) } + covered = outlines.reduce(covered) { $0 + straddles($1, c) } + covered = labels.reduce(covered) { $0 + area($1, c, 12) } return covered * 100 + lines.reduce(0) { $0 + area($1, c) } } return candidates.min { a, b in @@ -513,18 +529,28 @@ private struct Router { return out } - private func blocked(_ e: BoxEdge) -> [Bool] { + private func blocked(_ e: BoxEdge) -> (grid: [Bool], upright: [Bool]) { let open = Set(lineage(e.from) + lineage(e.to)) var grid = [Bool](repeating: false, count: cols * rows) + var upright = [Bool](repeating: false, count: cols * rows) for b in boxes { - let c = cells(open.contains(b.node.id) ? b.header : b.rect) + let isOpen = open.contains(b.node.id) + let c = cells(isOpen ? b.header : b.rect) for y in max(0, c.y0 - 1)...min(rows - 1, c.y1 + 1) { for x in max(0, c.x0 - 1)...min(cols - 1, c.x1 + 1) { grid[y * cols + x] = true } } + if isOpen { + let band = cells(CGRect(x: b.rect.minX, y: b.rect.minY, width: b.rect.width, height: b.header.height)) + for y in max(0, band.y0)...min(rows - 1, band.y1) { + for x in max(0, band.x0)...min(cols - 1, band.x1) { + upright[y * cols + x] = true + } + } + } } - return grid + return (grid, upright) } private func ports(_ id: String, _ grid: [Bool], outward: Bool) -> [Port] { @@ -557,7 +583,7 @@ private struct Router { } mutating func route(_ e: BoxEdge) throws(DiagramError) -> [CGPoint] { - let grid = blocked(e) + let (grid, upright) = blocked(e) var near = [Bool](repeating: false, count: cols * rows) for i in grid.indices where grid[i] { let x = i % cols @@ -628,7 +654,7 @@ private struct Router { continue } let n = ny * cols + nx - if grid[n] { + if grid[n] || (nd % 2 == 0 && (upright[n] || upright[c])) { continue } var step = stepCost + (nd == dir ? 0 : turnCost) + (near[n] ? nearCost : 0)