import AppKit /// Resolution-independent diagram renderer shared by PDF, PNG, SVG and macOS printing. /// The exported palette is intentionally independent of the user's macOS appearance. final class SchemaDiagramPrintView: NSView { struct Column { let name: String; let type: String; let primary: Bool } struct Entry { let name: String; let isView: Bool; let columns: [Column]; let origin: CGPoint } struct Link { let from: String; let to: String; let column: String; let target: String } let database: String let diagram: String let entries: [Entry] let links: [Link] let theme: String private let cardWidth: CGFloat = 260 private let headerHeight: CGFloat = 34 private let rowHeight: CGFloat = 19 private let margin: CGFloat = 55 private let titleHeight: CGFloat = 76 private let navy = NSColor(calibratedRed: 0.12, green: 0.20, blue: 0.34, alpha: 1) private var blue: NSColor { switch theme { case "Slate": return NSColor(calibratedRed: 0.30, green: 0.38, blue: 0.49, alpha: 1) case "Forest": return NSColor(calibratedRed: 0.16, green: 0.43, blue: 0.36, alpha: 1) case "Plum": return NSColor(calibratedRed: 0.42, green: 0.32, blue: 0.55, alpha: 1) default: return NSColor(calibratedRed: 0.12, green: 0.38, blue: 0.66, alpha: 1) } } private var violet: NSColor { blue.blended(withFraction: 0.25, of: .systemPurple) ?? blue } private let ink = NSColor(calibratedRed: 0.16, green: 0.23, blue: 0.32, alpha: 1) private let muted = NSColor(calibratedRed: 0.43, green: 0.49, blue: 0.57, alpha: 1) private let border = NSColor(calibratedRed: 0.79, green: 0.83, blue: 0.88, alpha: 1) init(database: String, diagram: String, entries: [Entry], links: [Link], theme: String = "Ocean") { self.database = database self.diagram = diagram self.entries = entries self.links = links self.theme = theme let width = max(900, (entries.map { $0.origin.x + 260 }.max() ?? 750) + 110) let height = max(650, (entries.map { $0.origin.y + CGFloat($0.columns.count) * 19 + 46 }.max() ?? 500) + 130) super.init(frame: CGRect(x: 0, y: 0, width: width, height: height)) } required init?(coder: NSCoder) { nil } override var isFlipped: Bool { true } private func cardHeight(_ entry: Entry) -> CGFloat { headerHeight + CGFloat(entry.columns.count) * rowHeight + 12 } private func cardRect(_ entry: Entry) -> CGRect { CGRect(x: entry.origin.x + margin, y: entry.origin.y + titleHeight, width: cardWidth, height: cardHeight(entry)) } private func text(_ string: String, at point: CGPoint, font: NSFont, color: NSColor) { (string as NSString).draw(at: point, withAttributes: [.font: font, .foregroundColor: color]) } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) NSColor.white.setFill() NSBezierPath(rect: bounds).fill() navy.setFill() NSBezierPath(rect: CGRect(x: 0, y: 0, width: bounds.width, height: 8)).fill() text(database, at: CGPoint(x: margin, y: 25), font: .systemFont(ofSize: 22, weight: .bold), color: navy) text("SCHEMA DIAGRAM / \(diagram)", at: CGPoint(x: margin, y: 54), font: .systemFont(ofSize: 10, weight: .medium), color: muted) text("\(entries.count) objects • \(links.count) relationships", at: CGPoint(x: margin, y: bounds.height - 27), font: .systemFont(ofSize: 10), color: muted) let byName = Dictionary(uniqueKeysWithValues: entries.map { ($0.name, $0) }) for link in links { guard let source = byName[link.from], let target = byName[link.to] else { continue } let a = cardRect(source), b = cardRect(target) let toRight = a.midX <= b.midX let start = CGPoint(x: toRight ? a.maxX : a.minX, y: a.midY) let end = CGPoint(x: toRight ? b.minX : b.maxX, y: b.midY) let curve = NSBezierPath() curve.move(to: start) curve.curve(to: end, controlPoint1: CGPoint(x: start.x + (toRight ? 55 : -55), y: start.y), controlPoint2: CGPoint(x: end.x + (toRight ? -55 : 55), y: end.y)) NSColor(calibratedRed: 0.39, green: 0.53, blue: 0.69, alpha: 1).setStroke() curve.lineWidth = 1.7 curve.stroke() } for entry in entries { let rect = cardRect(entry) NSColor.white.setFill() NSBezierPath(roundedRect: rect, xRadius: 7, yRadius: 7).fill() border.setStroke() NSBezierPath(roundedRect: rect, xRadius: 7, yRadius: 7).stroke() let header = CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: headerHeight) (entry.isView ? violet : blue).setFill() NSBezierPath(rect: header).fill() text(entry.name, at: CGPoint(x: rect.minX + 12, y: rect.minY + 9), font: .systemFont(ofSize: 12, weight: .semibold), color: .white) for (i, column) in entry.columns.enumerated() { let y = rect.minY + headerHeight + CGFloat(i) * rowHeight if i.isMultiple(of: 2) { NSColor(calibratedRed: 0.96, green: 0.97, blue: 0.99, alpha: 1).setFill() NSBezierPath(rect: CGRect(x: rect.minX + 1, y: y, width: rect.width - 2, height: rowHeight)).fill() } let label = (column.primary ? "◆ " : " ") + column.name text(String(label.prefix(29)), at: CGPoint(x: rect.minX + 9, y: y + 3), font: .systemFont(ofSize: 10, weight: column.primary ? .semibold : .regular), color: ink) let type = String(column.type.prefix(22)) as NSString let attrs: [NSAttributedString.Key: Any] = [.font: NSFont.systemFont(ofSize: 9), .foregroundColor: muted] let typeWidth = type.size(withAttributes: attrs).width type.draw(at: CGPoint(x: rect.maxX - typeWidth - 10, y: y + 4), withAttributes: attrs) } } } private func escape(_ text: String) -> String { text.replacingOccurrences(of: "&", with: "&") .replacingOccurrences(of: "<", with: "<") .replacingOccurrences(of: ">", with: ">") .replacingOccurrences(of: "\"", with: """) } func svgDocument() -> String { var svg = """ \(escape(database)) SCHEMA DIAGRAM / \(escape(diagram)) """ let byName = Dictionary(uniqueKeysWithValues: entries.map { ($0.name, $0) }) for link in links { guard let source = byName[link.from], let target = byName[link.to] else { continue } let a = cardRect(source), b = cardRect(target), right = a.midX <= b.midX let sx = right ? a.maxX : a.minX, ex = right ? b.minX : b.maxX let delta: CGFloat = right ? 55 : -55 svg += "\n" } let svgBlue = blue.usingColorSpace(.deviceRGB) ?? blue let svgViolet = violet.usingColorSpace(.deviceRGB) ?? violet func hex(_ color: NSColor) -> String { String(format: "#%02X%02X%02X", Int(color.redComponent * 255), Int(color.greenComponent * 255), Int(color.blueComponent * 255)) } for entry in entries { let r = cardRect(entry) svg += "\n" svg += "\n" svg += "\(escape(entry.name))\n" for (index, column) in entry.columns.enumerated() { let y = r.minY + 34 + CGFloat(index) * 19 if index.isMultiple(of: 2) { svg += "\n" } svg += "\(escape((column.primary ? "◆ " : " ") + column.name))\n" svg += "\(escape(column.type))\n" } } svg += "\(entries.count) objects / \(links.count) relationships\n" return svg } }