import CoreText import SwiftUI import UniformTypeIdentifiers private func hex(_ c: Color, _ scheme: ColorScheme) -> String { var env = EnvironmentValues() env.colorScheme = scheme let comps = c.resolve(in: env).cgColor.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .defaultIntent, options: nil)!.components! return "#" + comps.prefix(3).map { String(Int(($0 * 255).rounded()) + 0x100, radix: 16).dropFirst() }.joined() } private func num(_ v: CGFloat) -> String { "\((v * 10).rounded() / 10)" } private func esc(_ s: String) -> String { s.replacingOccurrences(of: "&", with: "&").replacingOccurrences(of: "<", with: "<").replacingOccurrences(of: ">", with: ">").replacingOccurrences(of: "\"", with: """) } private func json(_ v: Any) -> String { String(decoding: try! JSONSerialization.data(withJSONObject: v), as: UTF8.self).replacingOccurrences(of: " String { let url = Bundle.main.resourceURL!.appending(path: "fonts/atkinson-hyperlegible-next-\(subset)-wght-normal.woff2") let data = try! Data(contentsOf: url) return "@font-face{font-family:Atkinson;font-weight:200 800;src:url(data:font/woff2;base64,\(data.base64EncodedString())) format(\"woff2\")}" } private func edgePath(_ pts: [CGPoint]) -> String { guard let first = pts.first else { return "" } var d = "M\(num(first.x)) \(num(first.y))" 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 lin = hypot(b.x - a.x, b.y - a.y) let lout = hypot(c.x - b.x, c.y - b.y) let r = min(6, lin / 2, lout / 2) let p = CGPoint(x: b.x - (b.x - a.x) / lin * r, y: b.y - (b.y - a.y) / lin * r) let q = CGPoint(x: b.x + (c.x - b.x) / lout * r, y: b.y + (c.y - b.y) / lout * r) d += "L\(num(p.x)) \(num(p.y))Q\(num(b.x)) \(num(b.y)) \(num(q.x)) \(num(q.y))" } } let last = pts[pts.count - 1] return d + "L\(num(last.x)) \(num(last.y))" } private func arrowHead(_ from: CGPoint, _ to: CGPoint) -> String { let angle = atan2(to.y - from.y, to.x - from.x) let size: CGFloat = 8 let a = CGPoint(x: to.x - size * cos(angle - .pi / 7), y: to.y - size * sin(angle - .pi / 7)) let b = CGPoint(x: to.x - size * cos(angle + .pi / 7), y: to.y - size * sin(angle + .pi / 7)) return "\(num(to.x)),\(num(to.y)) \(num(a.x)),\(num(a.y)) \(num(b.x)),\(num(b.y))" } @MainActor func diagramHTML(_ layout: BoxLayout) -> String { let colors: [(String, Color)] = [("bg", Palette.bg), ("border", Palette.border), ("text", Palette.text), ("muted", Palette.muted), ("accent", Palette.accent)] let light = colors.map { "--\($0.0):\(hex($0.1, .light))" }.joined(separator: ";") let dark = colors.map { "--\($0.0):\(hex($0.1, .dark))" }.joined(separator: ";") let titleLine = textSize("X", titleFont).height let detailLine = textSize("X", detailFont).height var svg = "" for b in layout.boxes { let r = b.rect let f = fill(b) svg += "" svg += "" var y = r.minY + pad svg += "\(esc(b.node.title))" y += titleLine + 2 for d in b.node.details { svg += "\(esc(d))" y += detailLine } svg += "" } for (i, e) in layout.edges.enumerated() { svg += "" svg += "" if e.points.count >= 2 { svg += "" } if let label = e.edge.label, let rect = e.labelRect { svg += "\(esc(label))" } svg += "" } let data: [String: Any] = [ "boxes": layout.boxes.map { ["id": $0.node.id, "parent": $0.parent.map { $0 as Any } ?? NSNull()] }, "edges": layout.edges.map { ["from": $0.edge.from, "to": $0.edge.to] }, "presets": layout.presets.map { ["name": $0.name, "ids": Array($0.ids)] }, "width": layout.size.width, "height": layout.size.height, ] let buttons = layout.presets.enumerated().map { "" }.joined() return """ Diagram \(svg) """ } struct DiagramHTMLFile: FileDocument { static let readableContentTypes: [UTType] = [.html] let html: String init(html: String) { self.html = html } init(configuration: ReadConfiguration) throws { throw CocoaError(.fileReadUnsupportedScheme) } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { FileWrapper(regularFileWithContents: Data(html.utf8)) } }