Keep diagram arrow labels clear and route arrows around box headers
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user