import AppKit import CoreText import SwiftUI enum BoxTint: String { case plain, blue, green, amber, red } struct BoxNode { let id: String var title: String var details: [String] = [] var tint = BoxTint.plain var children: [BoxNode] = [] } struct BoxRow { var main: [BoxNode] = [] var aside: [BoxNode] = [] } struct BoxEdge { let from: String let to: String let label: String? let dashed: Bool } struct BoxPreset { let name: String let ids: Set } struct BoxDiagram { var rows: [BoxRow] = [] var edges: [BoxEdge] = [] var presets: [BoxPreset] = [] } private enum Token: Equatable { case word(String) case text(String) } private func tokenize(_ line: String) throws(DiagramError) -> [Token] { var out: [Token] = [] var rest = Substring(line) while true { rest = rest.drop { $0 == " " || $0 == "\t" } guard let c = rest.first else { return out } if c == "\"" { var text = "" var i = rest.index(after: rest.startIndex) var closed = false while i < rest.endIndex { let ch = rest[i] if ch == "\\", rest.index(after: i) < rest.endIndex { i = rest.index(after: i) text.append(rest[i]) } else if ch == "\"" { closed = true break } else { text.append(ch) } i = rest.index(after: i) } guard closed else { throw DiagramError(reason: "unclosed quote in \"\(line)\"") } out.append(.text(text)) rest = rest[rest.index(after: i)...] continue } let word = rest.prefix { $0 != " " && $0 != "\t" && $0 != "\"" } out.append(.word(String(word))) rest = rest.dropFirst(word.count) } } private let boxId = Re(#"^[A-Za-z0-9_-]+$"#) private final class NodeBuilder { var node: BoxNode var children: [NodeBuilder] = [] init(_ node: BoxNode) { self.node = node } func build() -> BoxNode { var n = node n.children = children.map { $0.build() } return n } } func parseBoxDiagram(_ src: String) throws(DiagramError) -> BoxDiagram { var rows: [(main: [NodeBuilder], aside: [NodeBuilder])] = [] var stack: [(indent: Int, node: NodeBuilder)] = [] var ids = Set() var diagram = BoxDiagram() for raw in src.components(separatedBy: "\n") { let line = raw.replacingOccurrences(of: "\t", with: " ") let body = trimmed(line) if body.isEmpty || body.hasPrefix("#") { continue } let indent = line.prefix { $0 == " " }.count var toks = try tokenize(body) if indent == 0 { stack = [] if toks == [.word("row")] { rows.append(([], [])) continue } if toks.first == .word("highlight") { guard toks.count >= 3, case .text(let name) = toks[1] else { throw DiagramError(reason: "a highlight needs a quoted name and box ids: \"\(body)\"") } var set = Set() for t in toks.dropFirst(2) { guard case .word(let id) = t else { throw DiagramError(reason: "can't parse \"\(body)\"") } set.insert(id) } diagram.presets.append(BoxPreset(name: name, ids: set)) continue } guard toks.count == 3 || toks.count == 4, case .word(let from) = toks[0], case .word(let arrow) = toks[1], case .word(let to) = toks[2], arrow == "->" || arrow == "..>" else { throw DiagramError(reason: "can't parse \"\(body)\"; top-level lines are row, highlight or an edge like a -> b \"label\"") } var label: String? if toks.count == 4 { guard case .text(let l) = toks[3] else { throw DiagramError(reason: "an edge's label must be quoted: \"\(body)\"") } label = l } diagram.edges.append(BoxEdge(from: from, to: to, label: label, dashed: arrow == "..>")) continue } guard !rows.isEmpty else { throw DiagramError(reason: "boxes go under a row: \"\(body)\"") } let aside = toks.first == .word("aside") if aside { toks.removeFirst() } guard case .word(let id) = toks.first, boxId.test(id) else { throw DiagramError(reason: "a box starts with its id: \"\(body)\"") } guard ids.insert(id).inserted else { throw DiagramError(reason: "box \(id) is defined twice") } var node = BoxNode(id: id, title: id) var texts: [String] = [] for t in toks.dropFirst() { switch t { case .text(let s): texts.append(s) case .word(let w): guard let tint = BoxTint(rawValue: w) else { throw DiagramError(reason: "\(w) isn't a color; use blue, green, amber, red or plain") } node.tint = tint } } if let first = texts.first { node.title = first node.details = Array(texts.dropFirst()) } let b = NodeBuilder(node) while let last = stack.last, last.indent >= indent { stack.removeLast() } if let parent = stack.last { guard !aside else { throw DiagramError(reason: "aside boxes sit directly under a row: \"\(body)\"") } parent.node.children.append(b) } else if aside { rows[rows.count - 1].aside.append(b) } else { rows[rows.count - 1].main.append(b) } stack.append((indent, b)) } diagram.rows = rows.map { BoxRow(main: $0.main.map { $0.build() }, aside: $0.aside.map { $0.build() }) } if ids.isEmpty { throw DiagramError(reason: "empty diagram") } for e in diagram.edges { for id in [e.from, e.to] where !ids.contains(id) { throw DiagramError(reason: "edge \(e.from) -> \(e.to) names no box \(id)") } } for p in diagram.presets { for id in p.ids where !ids.contains(id) { throw DiagramError(reason: "highlight \"\(p.name)\" names no box \(id)") } } return diagram } struct PlacedBox { let node: BoxNode let rect: CGRect let depth: Int let parent: String? let header: CGRect } struct RoutedEdge { let edge: BoxEdge let points: [CGPoint] let labelRect: CGRect? } struct BoxLayout: Identifiable { let id = UUID() let boxes: [PlacedBox] let index: [String: Int] let edges: [RoutedEdge] let presets: [BoxPreset] let size: CGSize func ancestors(_ id: String) -> [String] { var out: [String] = [] var p = boxes[index[id]!].parent while let id = p { out.append(id) p = boxes[index[id]!].parent } return out } func hit(_ p: CGPoint) -> String? { boxes.filter { $0.rect.contains(p) }.max { $0.depth < $1.depth }?.node.id } } struct BoxFocus { let boxes: Set let edges: Set static func box(_ id: String, _ layout: BoxLayout) -> BoxFocus { var boxes: Set = [id] var edges = Set() for (i, r) in layout.edges.enumerated() where r.edge.from == id || r.edge.to == id { edges.insert(i) boxes.insert(r.edge.from) boxes.insert(r.edge.to) } return BoxFocus(boxes: boxes, edges: edges) } static func preset(_ p: BoxPreset, _ layout: BoxLayout) -> BoxFocus { let edges = layout.edges.indices.filter { p.ids.contains(layout.edges[$0].edge.from) && p.ids.contains(layout.edges[$0].edge.to) } return BoxFocus(boxes: p.ids, edges: Set(edges)) } func dims(_ b: PlacedBox, _ layout: BoxLayout) -> Bool { if boxes.contains(b.node.id) || layout.ancestors(b.node.id).contains(where: boxes.contains) { return false } return !boxes.contains { layout.ancestors($0).contains(b.node.id) } } } private let pad: CGFloat = 12 private let childGap: CGFloat = 14 private let boxGap: CGFloat = 20 private let rowGap: CGFloat = 48 private let columnGap: CGFloat = 80 private let margin: CGFloat = 20 @MainActor private var titleFont: CTFont { Fonts.ns(13, weight: 600) as CTFont } @MainActor private var detailFont: CTFont { Fonts.ns(12) as CTFont } @MainActor private var edgeLabelFont: CTFont { Fonts.ns(11) as CTFont } @MainActor private func headerSize(_ n: BoxNode) -> CGSize { let t = textSize(n.title, titleFont) var w = t.width var h = t.height for d in n.details { let s = textSize(d, detailFont) w = max(w, s.width) h += s.height } return CGSize(width: w, height: h + (n.details.isEmpty ? 0 : 2)) } @MainActor private struct Sizer { let labelGaps: [String: CGFloat] init(_ d: BoxDiagram) { var gaps: [String: CGFloat] = [:] for e in d.edges { guard let label = e.label else { continue } let key = Sizer.pair(e.from, e.to) gaps[key] = max(gaps[key] ?? 0, textSize(label, edgeLabelFont).width + 16) } labelGaps = gaps } static func pair(_ a: String, _ b: String) -> String { a < b ? "\(a) \(b)" : "\(b) \(a)" } func gaps(_ nodes: [BoxNode], _ base: CGFloat) -> [CGFloat] { zip(nodes, nodes.dropFirst()).map { a, b in max(base, labelGaps[Sizer.pair(a.id, b.id)] ?? 0) } } func naturalSize(_ n: BoxNode) -> CGSize { let head = headerSize(n) if n.children.isEmpty { return CGSize(width: head.width + pad * 2, height: head.height + pad * 2) } let kids = n.children.map(naturalSize) let row = kids.map(\.width).reduce(0, +) + gaps(n.children, childGap).reduce(0, +) return CGSize(width: max(head.width, row) + pad * 2, height: pad + head.height + pad + kids.map(\.height).max()! + pad) } func stretch(_ nodes: [BoxNode], to width: CGFloat, gaps: [CGFloat]) -> [CGFloat] { let natural = nodes.map { naturalSize($0).width } let sum = natural.reduce(0, +) let extra = width - sum - gaps.reduce(0, +) return natural.map { $0 + extra * $0 / sum } } func place(_ n: BoxNode, _ rect: CGRect, _ depth: Int, _ parent: String?, _ out: inout [PlacedBox]) { let head = headerSize(n) let header = n.children.isEmpty ? rect : CGRect(x: rect.minX, y: rect.minY, width: min(rect.width, head.width + pad * 2), height: pad + head.height) out.append(PlacedBox(node: n, rect: rect, depth: depth, parent: parent, header: header)) if n.children.isEmpty { return } let gaps = gaps(n.children, childGap) let widths = stretch(n.children, to: rect.width - pad * 2, gaps: gaps) let top = rect.minY + pad + head.height + pad var x = rect.minX + pad for (i, (c, w)) in zip(n.children, widths).enumerated() { place(c, CGRect(x: x, y: top, width: w, height: rect.maxY - pad - top), depth + 1, n.id, &out) x += w + (i < gaps.count ? gaps[i] : 0) } } func rowWidth(_ r: BoxRow) -> CGFloat { r.main.map { naturalSize($0).width }.reduce(0, +) + gaps(r.main, boxGap).reduce(0, +) } } @MainActor 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 var boxes: [PlacedBox] = [] var y = margin var asideBottom = margin - rowGap var bottom = margin for r in d.rows { let height = r.main.map { sizer.naturalSize($0).height }.max() ?? 0 let gaps = sizer.gaps(r.main, boxGap) var x = margin for (i, (n, w)) in zip(r.main, sizer.stretch(r.main, to: mainWidth, gaps: gaps)).enumerated() { sizer.place(n, CGRect(x: x, y: y, width: w, height: height), 0, nil, &boxes) x += w + (i < gaps.count ? gaps[i] : 0) } var ay = max(y, asideBottom + rowGap) for n in r.aside { let h = sizer.naturalSize(n).height sizer.place(n, CGRect(x: asideX, y: ay, width: asideWidth, height: h), 0, nil, &boxes) ay += h + rowGap asideBottom = ay - rowGap } bottom = max(bottom, y + height, asideBottom) y += height + (r.main.isEmpty ? 0 : rowGap) } let width = (asideWidth > 0 ? asideX + asideWidth : margin + mainWidth) + margin let size = CGSize(width: width, height: bottom + margin) let index = Dictionary(uniqueKeysWithValues: boxes.enumerated().map { ($0.element.node.id, $0.offset) }) var router = Router(boxes: boxes, index: index, size: size) let order = d.edges.indices.sorted { a, b in router.span(d.edges[a]) < router.span(d.edges[b]) } var points = [[CGPoint]](repeating: [], count: d.edges.count) for i in order { points[i] = try router.route(d.edges[i]) } var solid: [CGRect] = boxes.map(\.header) 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) labelRect = r } routed.append(RoutedEdge(edge: e, points: pts, labelRect: labelRect)) } 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 { 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) } for (a, b) in segs { for t in stride(from: 0.5, through: 0.95, by: 0.05).flatMap({ $0 == 0.5 ? [$0] : [$0, 1 - $0] }) { let p = CGPoint(x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t) if a.x == b.x { candidates.append((CGRect(x: p.x + 6, y: p.y - size.height / 2, width: size.width, height: size.height), candidates.count)) candidates.append((CGRect(x: p.x - 6 - size.width, y: p.y - size.height / 2, width: size.width, height: size.height), candidates.count)) } else { candidates.append((CGRect(x: p.x - size.width / 2, y: p.y - 3 - size.height, width: size.width, height: size.height), candidates.count)) candidates.append((CGRect(x: p.x - size.width / 2, y: p.y + 3, width: size.width, height: size.height), candidates.count)) } } } func area(_ a: CGRect, _ b: CGRect) -> CGFloat { let i = a.insetBy(dx: -2, dy: -2).intersection(b) return i.isNull ? 0 : max(1, i.width * i.height) } 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) } return covered * 100 + lines.reduce(0) { $0 + area($1, c) } } return candidates.min { a, b in let oa = overlap(a.rect) let ob = overlap(b.rect) return oa != ob ? oa < ob : a.order < b.order }!.rect } private let cell: CGFloat = 4 private let stepCost = 10 private let turnCost = 60 private let nearCost = 4 private let parallelCost = 80 private let laneCost = 40 private let laneWidth = 3 private let crossCost = 15 private let dx = [1, 0, -1, 0] private let dy = [0, 1, 0, -1] private struct Port { let cell: Int let dir: Int let border: CGPoint let cost: Int } private struct Router { let boxes: [PlacedBox] let index: [String: Int] let cols: Int let rows: Int var used: [UInt8] var lanes: [UInt8] init(boxes: [PlacedBox], index: [String: Int], size: CGSize) { self.boxes = boxes self.index = index cols = Int(ceil(size.width / cell)) rows = Int(ceil(size.height / cell)) used = [UInt8](repeating: 0, count: cols * rows) lanes = [UInt8](repeating: 0, count: cols * rows) } func span(_ e: BoxEdge) -> CGFloat { let a = boxes[index[e.from]!].rect let b = boxes[index[e.to]!].rect return abs(a.midX - b.midX) + abs(a.midY - b.midY) } private func cells(_ r: CGRect) -> (x0: Int, y0: Int, x1: Int, y1: Int) { (Int(floor(r.minX / cell)), Int(floor(r.minY / cell)), Int(ceil(r.maxX / cell)) - 1, Int(ceil(r.maxY / cell)) - 1) } private func lineage(_ id: String) -> [String] { var out: [String] = [] var p = boxes[index[id]!].parent while let id = p { out.append(id) p = boxes[index[id]!].parent } return out } private func blocked(_ e: BoxEdge) -> [Bool] { let open = Set(lineage(e.from) + lineage(e.to)) var grid = [Bool](repeating: false, count: cols * rows) for b in boxes { let c = cells(open.contains(b.node.id) ? 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 } } } return grid } private func ports(_ id: String, _ grid: [Bool], outward: Bool) -> [Port] { let r = boxes[index[id]!].rect let c = cells(r) var out: [Port] = [] func add(_ x: Int, _ y: Int, _ dir: Int, _ border: CGPoint, _ offset: Int) { guard x >= 0, y >= 0, x < cols, y < rows, !grid[y * cols + x] else { return } out.append(Port(cell: y * cols + x, dir: outward ? dir : (dir + 2) % 4, border: border, cost: offset * 2)) } let midX = (c.x0 + c.x1) / 2 let midY = (c.y0 + c.y1) / 2 if c.x1 - c.x0 >= 2 { for x in (c.x0 + 1)...(c.x1 - 1) { let px = (CGFloat(x) + 0.5) * cell add(x, c.y0 - 2, 3, CGPoint(x: px, y: r.minY), abs(x - midX)) add(x, c.y1 + 2, 1, CGPoint(x: px, y: r.maxY), abs(x - midX)) } } if c.y1 - c.y0 >= 2 { for y in (c.y0 + 1)...(c.y1 - 1) { let py = (CGFloat(y) + 0.5) * cell add(c.x0 - 2, y, 2, CGPoint(x: r.minX, y: py), abs(y - midY)) add(c.x1 + 2, y, 0, CGPoint(x: r.maxX, y: py), abs(y - midY)) } } return out } mutating func route(_ e: BoxEdge) throws(DiagramError) -> [CGPoint] { let grid = blocked(e) var near = [Bool](repeating: false, count: cols * rows) for i in grid.indices where grid[i] { let x = i % cols let y = i / cols for yy in max(0, y - 2)...min(rows - 1, y + 2) { for xx in max(0, x - 2)...min(cols - 1, x + 2) { near[yy * cols + xx] = true } } } let starts = ports(e.from, grid, outward: true) let goals = ports(e.to, grid, outward: false) guard !starts.isEmpty, !goals.isEmpty, e.from != e.to else { throw DiagramError(reason: "can't route \(e.from) -> \(e.to)") } var goalAt: [Int: Port] = [:] for g in goals { goalAt[g.cell] = g } let gx0 = goals.map { $0.cell % cols }.min()! let gx1 = goals.map { $0.cell % cols }.max()! let gy0 = goals.map { $0.cell / cols }.min()! let gy1 = goals.map { $0.cell / cols }.max()! func h(_ c: Int) -> Int { let x = c % cols let y = c / cols return stepCost * (max(0, gx0 - x, x - gx1) + max(0, gy0 - y, y - gy1)) } let states = cols * rows * 4 var cost = [Int](repeating: .max, count: states) var from = [Int](repeating: -1, count: states) var heap = Heap() var startAt: [Int: Port] = [:] for s in starts { let st = s.cell * 4 + s.dir if s.cost < cost[st] { cost[st] = s.cost startAt[st] = s heap.push(s.cost + h(s.cell), st) } } var best = Int.max var bestState = -1 while let top = heap.pop() { let (f, st) = top if f >= best { break } let g = cost[st] if f > g + h(st / 4) { continue } let c = st / 4 let dir = st % 4 if let goal = goalAt[c] { let total = g + (dir == goal.dir ? 0 : turnCost) if total < best { best = total bestState = st } } let x = c % cols let y = c / cols for nd in 0..<4 where nd != (dir + 2) % 4 { let nx = x + dx[nd] let ny = y + dy[nd] guard nx >= 0, ny >= 0, nx < cols, ny < rows else { continue } let n = ny * cols + nx if grid[n] { continue } var step = stepCost + (nd == dir ? 0 : turnCost) + (near[n] ? nearCost : 0) let axis: UInt8 = nd % 2 == 0 ? 1 : 2 if used[n] & axis != 0 { step += parallelCost } else if lanes[n] & axis != 0 { step += laneCost } else if used[n] != 0 { step += crossCost } let ns = n * 4 + nd if g + step < cost[ns] { cost[ns] = g + step from[ns] = st heap.push(g + step + h(n), ns) } } } guard bestState >= 0 else { throw DiagramError(reason: "can't route \(e.from) -> \(e.to)") } var path: [Int] = [] var st = bestState while st >= 0 { path.append(st) st = from[st] } path.reverse() for (a, b) in zip(path, path.dropFirst()) { let axis: UInt8 = (b % 4) % 2 == 0 ? 1 : 2 used[a / 4] |= axis used[b / 4] |= axis for c in [a / 4, b / 4] { let x = c % cols let y = c / cols for k in 1...laneWidth { for (nx, ny) in axis == 1 ? [(x, y - k), (x, y + k)] : [(x - k, y), (x + k, y)] where nx >= 0 && ny >= 0 && nx < cols && ny < rows { lanes[ny * cols + nx] |= axis } } } } func center(_ c: Int) -> CGPoint { CGPoint(x: (CGFloat(c % cols) + 0.5) * cell, y: (CGFloat(c / cols) + 0.5) * cell) } var pts = [startAt[path[0]]!.border] pts += path.map { center($0 / 4) } pts.append(goalAt[bestState / 4]!.border) return simplify(pts) } } private func simplify(_ pts: [CGPoint]) -> [CGPoint] { var out: [CGPoint] = [] for p in pts { if let last = out.last, last == p { continue } if out.count >= 2 { let a = out[out.count - 2] let b = out[out.count - 1] if (a.x == b.x && b.x == p.x) || (a.y == b.y && b.y == p.y) { out[out.count - 1] = p continue } } out.append(p) } return out } private struct Heap { private var items: [(Int, Int)] = [] mutating func push(_ key: Int, _ value: Int) { items.append((key, value)) var i = items.count - 1 while i > 0 { let p = (i - 1) / 2 if items[p].0 <= items[i].0 { break } items.swapAt(p, i) i = p } } mutating func pop() -> (Int, Int)? { guard let top = items.first else { return nil } let last = items.removeLast() if !items.isEmpty { items[0] = last var i = 0 while true { let l = i * 2 + 1 let r = l + 1 var m = i if l < items.count && items[l].0 < items[m].0 { m = l } if r < items.count && items[r].0 < items[m].0 { m = r } if m == i { break } items.swapAt(m, i) i = m } } return top } } @MainActor private var layoutCache: [String: Result] = [:] @MainActor func cachedBoxLayout(_ source: String) -> Result { let key = "\(FontSize.shared.body)\n\(source)" if let hit = layoutCache[key] { return hit } let result = Result(catching: { () throws(DiagramError) -> BoxLayout in try layoutBoxDiagram(parseBoxDiagram(source)) }) if layoutCache.count > 64 { layoutCache = [:] } layoutCache[key] = result return result } private func fill(_ b: PlacedBox) -> Color { switch b.node.tint { case .blue: Palette.accentTint case .green: Palette.okTint case .amber: Palette.warnTint case .red: Palette.dangerTint case .plain: b.depth == 0 ? Palette.surface : Palette.raised } } struct BoxCanvas: View { let layout: BoxLayout var focus: BoxFocus? var body: some View { Canvas { context, size in let scale = layout.size.width > 0 ? size.width / layout.size.width : 1 context.scaleBy(x: scale, y: scale) draw(&context) } } private func draw(_ context: inout GraphicsContext) { let titleLine = textSize("X", titleFont).height let detailLine = textSize("X", detailFont).height for b in layout.boxes { var ctx = context if let focus, focus.dims(b, layout) { ctx.opacity = 0.3 } let path = Path(roundedRect: b.rect, cornerRadius: b.depth == 0 ? 8 : 6) ctx.fill(path, with: .color(fill(b))) let lit = focus?.boxes.contains(b.node.id) == true ctx.stroke(path, with: .color(lit ? Palette.accent : Palette.border), lineWidth: lit ? 1.5 : 1) var y = b.rect.minY + pad ctx.draw(Text(b.node.title).font(Font(titleFont)).foregroundColor(Palette.text), at: CGPoint(x: b.rect.minX + pad, y: y), anchor: .topLeading) y += titleLine + 2 for d in b.node.details { ctx.draw(Text(d).font(Font(detailFont)).foregroundColor(Palette.muted), at: CGPoint(x: b.rect.minX + pad, y: y), anchor: .topLeading) y += detailLine } } for (i, r) in layout.edges.enumerated() { var ctx = context let lit = focus?.edges.contains(i) == true if focus != nil && !lit { ctx.opacity = 0.2 } let color = lit ? Palette.accent : Palette.muted ctx.stroke(rounded(r.points), with: .color(color), style: StrokeStyle(lineWidth: lit ? 2 : 1.3, lineCap: .round, lineJoin: .round, dash: r.edge.dashed ? [5, 4] : [])) if r.points.count >= 2 { arrow(&ctx, r.points[r.points.count - 2], r.points[r.points.count - 1], color) } if let label = r.edge.label, let rect = r.labelRect { ctx.draw(Text(label).font(Font(edgeLabelFont)).foregroundColor(lit ? Palette.accent : Palette.muted), at: CGPoint(x: rect.minX, y: rect.minY), anchor: .topLeading) } } } private func rounded(_ pts: [CGPoint]) -> Path { var p = Path() guard let first = pts.first else { return p } p.move(to: first) if pts.count > 2 { for i in 1..<(pts.count - 1) { let a = pts[i - 1] let b = pts[i] let c = pts[i + 1] let r = min(6, hypot(b.x - a.x, b.y - a.y) / 2, hypot(c.x - b.x, c.y - b.y) / 2) p.addArc(tangent1End: b, tangent2End: c, radius: r) } } p.addLine(to: pts[pts.count - 1]) return p } private func arrow(_ context: inout GraphicsContext, _ from: CGPoint, _ to: CGPoint, _ color: Color) { let angle = atan2(to.y - from.y, to.x - from.x) let size: CGFloat = 8 var head = Path() head.move(to: to) head.addLine(to: CGPoint(x: to.x - size * cos(angle - .pi / 7), y: to.y - size * sin(angle - .pi / 7))) head.addLine(to: CGPoint(x: to.x - size * cos(angle + .pi / 7), y: to.y - size * sin(angle + .pi / 7))) head.closeSubpath() context.fill(head, with: .color(color)) } } struct BoxDiagramView: View { let source: String @Environment(Overlays.self) private var overlays var body: some View { switch cachedBoxLayout(source) { case .success(let layout): FitWidth(natural: layout.size) { BoxCanvas(layout: layout) } .contentShape(Rectangle()) .onTapGesture { overlays.diagram = layout } .help("Open full size") .frame(maxWidth: .infinity, alignment: .leading) case .failure(let error): DiagramFailure(source: source, reason: error.reason) } } } struct DiagramViewer: View { let layout: BoxLayout @ViewState private var picked: String? @ViewState private var preset: Int? private var focus: BoxFocus? { if let picked { return .box(picked, layout) } if let preset { return .preset(layout.presets[preset], layout) } return nil } var body: some View { VStack(alignment: .leading, spacing: 8) { if !layout.presets.isEmpty { HStack(spacing: 6) { ForEach(Array(layout.presets.enumerated()), id: \.offset) { i, p in Button(p.name) { picked = nil preset = preset == i ? nil : i } .buttonStyle(WebButtonStyle(primary: preset == i)) } } } ZoomingCanvas(layout: layout, focus: focus) { id in preset = nil picked = id == picked ? nil : id } } .padding(12) .background(Palette.raised, in: RoundedRectangle(cornerRadius: 10)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(Palette.border)) .shadow(color: Palette.overlayShadow, radius: 14, y: 8) } } private struct TappableCanvas: View { let layout: BoxLayout let focus: BoxFocus? let tap: (String?) -> Void var body: some View { BoxCanvas(layout: layout, focus: focus) .frame(width: layout.size.width, height: layout.size.height) .contentShape(Rectangle()) .onTapGesture(coordinateSpace: .local) { p in tap(layout.hit(p)) } } } private final class CenteringClipView: NSClipView { override func constrainBoundsRect(_ proposedBounds: NSRect) -> NSRect { var r = super.constrainBoundsRect(proposedBounds) guard let doc = documentView else { return r } if r.width > doc.frame.width { r.origin.x = (doc.frame.width - r.width) / 2 } if r.height > doc.frame.height { r.origin.y = (doc.frame.height - r.height) / 2 } return r } } private final class FittingScrollView: NSScrollView { var fitted = false override func layout() { super.layout() guard !fitted, let doc = documentView, bounds.width > 0, bounds.height > 0, doc.frame.width > 0 else { return } fitted = true magnification = min(2, bounds.width / doc.frame.width, bounds.height / doc.frame.height) } } private struct ZoomingCanvas: NSViewRepresentable { let layout: BoxLayout let focus: BoxFocus? let tap: (String?) -> Void func makeNSView(context: Context) -> FittingScrollView { let scroll = FittingScrollView() scroll.contentView = CenteringClipView() scroll.drawsBackground = false scroll.hasVerticalScroller = true scroll.hasHorizontalScroller = true scroll.autohidesScrollers = true scroll.allowsMagnification = true scroll.minMagnification = 0.25 scroll.maxMagnification = 6 let host = NSHostingView(rootView: TappableCanvas(layout: layout, focus: focus, tap: tap)) host.frame = CGRect(origin: .zero, size: layout.size) scroll.documentView = host return scroll } func updateNSView(_ scroll: FittingScrollView, context: Context) { (scroll.documentView as? NSHostingView)?.rootView = TappableCanvas(layout: layout, focus: focus, tap: tap) } }