import Foundation #if os(iOS) import UIKit #elseif os(OSX) import AppKit #endif class RenderUtils { class func mapDash(_ dashes: [Double]) -> UnsafeMutablePointer { let p = UnsafeMutablePointer.allocate(capacity: dashes.count * MemoryLayout.size) for (index, item) in dashes.enumerated() { p[index] = CGFloat(item) } return p } class func createNodeRenderer(_ node: Node, view: DrawingView?, parentRenderer: GroupRenderer? = nil) -> NodeRenderer { if let group = node as? Group { return GroupRenderer(group: group, view: view, parentRenderer: parentRenderer) } else if let shape = node as? Shape { return ShapeRenderer(shape: shape, view: view, parentRenderer: parentRenderer) } else if let text = node as? Text { return TextRenderer(text: text, view: view, parentRenderer: parentRenderer) } else if let image = node as? Image { return ImageRenderer(image: image, view: view, parentRenderer: parentRenderer) } fatalError("Unsupported node: \(node)") } static let availableFonts = prepareFonts() static func prepareFonts() -> [String: String] { var result = [String: String]() for family in MFont.mFamilyNames { // TODO: do we need to include family names? result[family.lowercased()] = family for font in MFont.mFontNames(forFamily: family) { result[font.lowercased()] = font } } result["serif"] = "Georgia" result["sans-serif"] = "Arial" result["monospace"] = "Courier" return result } class func loadFont(name: String, size: Int, weight: String?) -> MFont? { guard let fontName = findFontName(title: name) else { return nil } var fontDesc = MFontDescriptor(name: fontName, size: CGFloat(size)) let lowerWeight = weight?.lowercased() if lowerWeight == "bold" || lowerWeight == "bolder" { #if os(iOS) if let boldDesc = fontDesc.withSymbolicTraits(.traitBold) { fontDesc = boldDesc } #elseif os(OSX) fontDesc = fontDesc.withSymbolicTraits(.bold) #endif } return MFont(descriptor: fontDesc, size: CGFloat(size)) } static func findFontName(title: String) -> String? { let fonts = title.split(separator: ",").map { String($0).trimmingCharacters(in: CharacterSet(charactersIn: " '")) } for font in fonts { if let availableFont = availableFonts[font.lowercased()] { return availableFont } } return nil } class func applyOpacity(_ color: Color, opacity: Double) -> Color { return Color.rgba(r: color.r(), g: color.g(), b: color.b(), a: Double(color.a()) / 255.0 * opacity) } class func toCGPath(_ locus: Locus) -> CGPath { if let arc = locus as? Arc { if arc.ellipse.rx != arc.ellipse.ry { // http://stackoverflow.com/questions/11365775/how-to-draw-an-elliptical-arc-with-coregraphics // input parameters let ellipse = arc.ellipse let startAngle = CGFloat(arc.shift) let endAngle = startAngle + CGFloat(arc.extent) let r = CGFloat(ellipse.rx) let scale = CGFloat(ellipse.ry / ellipse.rx) let path = CGMutablePath() var t = CGAffineTransform(translationX: CGFloat(ellipse.cx), y: CGFloat(ellipse.cy)) t = CGAffineTransform(scaleX: 1.0, y: scale).concatenating(t) path.addArc(center: CGPoint.zero, radius: r, startAngle: startAngle, endAngle: endAngle, clockwise: false, transform: t) return path } } return toBezierPath(locus).cgPath } class func toBezierPath(_ locus: Locus) -> MBezierPath { if let round = locus as? RoundRect { let corners = CGSize(width: CGFloat(round.rx), height: CGFloat(round.ry)) return MBezierPath(roundedRect: round.rect.toCG(), byRoundingCorners: MRectCorner.allCorners, cornerRadii: corners) } else if let arc = locus as? Arc { if arc.ellipse.rx == arc.ellipse.ry { return arcToPath(arc) } } else if let point = locus as? Point { let path = MBezierPath() path.move(to: CGPoint(x: CGFloat(point.x), y: CGFloat(point.y))) path.addLine(to: CGPoint(x: CGFloat(point.x), y: CGFloat(point.y))) return path } else if let line = locus as? Line { let path = MBezierPath() path.move(to: CGPoint(x: CGFloat(line.x1), y: CGFloat(line.y1))) path.addLine(to: CGPoint(x: CGFloat(line.x2), y: CGFloat(line.y2))) return path } else if let polygon = locus as? Polygon { let path = pointsToPath(polygon.points) path.close() return path } else if let polygon = locus as? Polyline { return pointsToPath(polygon.points) } else if let rect = locus as? Rect { return MBezierPath(rect: rect.toCG()) } else if let circle = locus as? Circle { return MBezierPath(ovalIn: circle.bounds().toCG()) } else if let path = locus as? Path { return toBezierPath(path) } else if let transformedLocus = locus as? TransformedLocus { let path = toBezierPath(transformedLocus.locus) path.apply(transformedLocus.transform.toCG()) return path } else if let ellipse = locus as? Ellipse { return MBezierPath(ovalIn: ellipse.bounds().toCG()) } fatalError("Unsupported locus: \(locus)") } fileprivate class func arcToPath(_ arc: Arc) -> MBezierPath { let shift = CGFloat(arc.shift) let end = shift + CGFloat(arc.extent) let ellipse = arc.ellipse let center = CGPoint(x: CGFloat(ellipse.cx), y: CGFloat(ellipse.cy)) return MBezierPath(arcCenter: center, radius: CGFloat(ellipse.rx), startAngle: shift, endAngle: end, clockwise: true) } fileprivate class func pointsToPath(_ points: [Double]) -> MBezierPath { let count = points.count / 2 * 2 // points count divisible by 2 let parts = stride(from: 0, to: count, by: 2).map { Array(points[$0 ..< $0 + 2]) } let path = MBezierPath() var first = true for part in parts { let point = CGPoint(x: CGFloat(part[0]), y: CGFloat(part[1])) if first { path.move(to: point) first = false } else { path.addLine(to: point) } } return path } fileprivate class func toBezierPath(_ path: Path) -> MBezierPath { let bezierPath = MBezierPath() var currentPoint: CGPoint? var cubicPoint: CGPoint? var quadrPoint: CGPoint? var initialPoint: CGPoint? func M(_ x: Double, y: Double) { let point = CGPoint(x: CGFloat(x), y: CGFloat(y)) bezierPath.move(to: point) setInitPoint(point) } func m(_ x: Double, y: Double) { if let cur = currentPoint { let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) bezierPath.move(to: next) setInitPoint(next) } else { M(x, y: y) } } func L(_ x: Double, y: Double) { lineTo(CGPoint(x: CGFloat(x), y: CGFloat(y))) } func l(_ x: Double, y: Double) { if let cur = currentPoint { lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y)) } else { L(x, y: y) } } func H(_ x: Double) { if let cur = currentPoint { lineTo(CGPoint(x: CGFloat(x), y: CGFloat(cur.y))) } } func h(_ x: Double) { if let cur = currentPoint { lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(cur.y))) } } func V(_ y: Double) { if let cur = currentPoint { lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y))) } } func v(_ y: Double) { if let cur = currentPoint { lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y) + cur.y)) } } func lineTo(_ p: CGPoint) { bezierPath.addLine(to: p) setPoint(p) } func c(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) { if let cur = currentPoint { let endPoint = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) let controlPoint1 = CGPoint(x: CGFloat(x1) + cur.x, y: CGFloat(y1) + cur.y) let controlPoint2 = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y) bezierPath.addCurve(to: endPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) setCubicPoint(endPoint, cubic: controlPoint2) } } func C(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) { let endPoint = CGPoint(x: CGFloat(x), y: CGFloat(y)) let controlPoint1 = CGPoint(x: CGFloat(x1), y: CGFloat(y1)) let controlPoint2 = CGPoint(x: CGFloat(x2), y: CGFloat(y2)) bezierPath.addCurve(to: endPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) setCubicPoint(endPoint, cubic: controlPoint2) } func s(_ x2: Double, y2: Double, x: Double, y: Double) { if let cur = currentPoint { let nextCubic = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y) let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) var xy1: CGPoint? if let curCubicVal = cubicPoint { xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y) } else { xy1 = cur } bezierPath.addCurve(to: next, controlPoint1: xy1!, controlPoint2: nextCubic) setCubicPoint(next, cubic: nextCubic) } } func S(_ x2: Double, y2: Double, x: Double, y: Double) { if let cur = currentPoint { let nextCubic = CGPoint(x: CGFloat(x2), y: CGFloat(y2)) let next = CGPoint(x: CGFloat(x), y: CGFloat(y)) var xy1: CGPoint? if let curCubicVal = cubicPoint { xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y) } else { xy1 = cur } bezierPath.addCurve(to: next, controlPoint1: xy1!, controlPoint2: nextCubic) setCubicPoint(next, cubic: nextCubic) } } func q(_ x1: Double, y1: Double, x: Double, y: Double) { if let cur = currentPoint { let dx = Double(cur.x) let dy = Double(cur.y) Q(x1 + dx, y1: y1 + dy, x: x + dx, y: y + dy) } } func Q(_ x1: Double, y1: Double, x: Double, y: Double) { let endPoint = CGPoint(x: x, y: y) let controlPoint = CGPoint(x: x1, y: y1) bezierPath.addQuadCurve(to: endPoint, controlPoint: controlPoint) setQuadrPoint(endPoint, quadr: controlPoint) } func t(_ x: Double, y: Double) { if let cur = currentPoint { let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) var quadr: CGPoint? if let curQuadr = quadrPoint { quadr = CGPoint(x: 2 * cur.x - curQuadr.x, y: 2 * cur.y - curQuadr.y) } else { quadr = cur } bezierPath.addQuadCurve(to: next, controlPoint: quadr!) setQuadrPoint(next, quadr: quadr!) } } func T(_ x: Double, y: Double) { if let cur = currentPoint { let next = CGPoint(x: CGFloat(x), y: CGFloat(y)) var quadr: CGPoint? if let curQuadr = quadrPoint { quadr = CGPoint(x: 2 * cur.x - curQuadr.x, y: 2 * cur.y - curQuadr.y) } else { quadr = cur } bezierPath.addQuadCurve(to: next, controlPoint: quadr!) setQuadrPoint(next, quadr: quadr!) } } func a(_ rx: Double, ry: Double, angle: Double, largeArc: Bool, sweep: Bool, x: Double, y: Double) { if let cur = currentPoint { A(rx, ry: ry, angle: angle, largeArc: largeArc, sweep: sweep, x: x + Double(cur.x), y: y + Double(cur.y)) } } func A(_ _rx: Double, ry _ry: Double, angle _angle: Double, largeArc: Bool, sweep: Bool, x: Double, y: Double) { let angle = _angle * .pi / 180 if let cur = currentPoint { let x1 = Double(cur.x) let y1 = Double(cur.y) // find arc center coordinates and points angles as per // http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter if _rx == 0 || _ry == 0 { L(x, y: y) } else { var rx = abs(_rx) var ry = abs(_ry) let x1_ = cos(angle) * (x1 - x) / 2 + sin(angle) * (y1 - y) / 2 let y1_ = -1 * sin(angle) * (x1 - x) / 2 + cos(angle) * (y1 - y) / 2 let rCheck = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry) if rCheck > 1 { rx = sqrt(rCheck) * rx ry = sqrt(rCheck) * ry } // make sure the value under the root is positive let underroot = (rx * rx * ry * ry - rx * rx * y1_ * y1_ - ry * ry * x1_ * x1_) / (rx * rx * y1_ * y1_ + ry * ry * x1_ * x1_) var bigRoot = (underroot > 0) ? sqrt(underroot) : 0 bigRoot = (bigRoot <= 1e-2) ? 0 : bigRoot let coef: Double = (sweep != largeArc) ? 1 : -1 let cx_ = coef * bigRoot * rx * y1_ / ry let cy_ = -1 * coef * bigRoot * ry * x1_ / rx let cx = cos(angle) * cx_ - sin(angle) * cy_ + (x1 + x) / 2 let cy = sin(angle) * cx_ + cos(angle) * cy_ + (y1 + y) / 2 let t1 = calcAngle(ux: 1, uy: 0, vx: (x1_ - cx_) / rx, vy: (y1_ - cy_) / ry) var delta = calcAngle(ux: (x1_ - cx_) / rx, uy: (y1_ - cy_) / ry, vx: (-x1_ - cx_) / rx, vy: (-y1_ - cy_) / ry) let pi2 = Double.pi * 2 if delta > 0 { delta = delta.truncatingRemainder(dividingBy: pi2) if !sweep { delta -= pi2 } } else if delta < 0 { delta = -1 * ((-1 * delta).truncatingRemainder(dividingBy: pi2)) if sweep { delta += pi2 } } E(cx - rx, y: cy - ry, w: 2 * rx, h: 2 * ry, startAngle: t1, arcAngle: delta, rotation: angle) setPoint(CGPoint(x: CGFloat(x), y: CGFloat(y))) } } } func E(_ x: Double, y: Double, w: Double, h: Double, startAngle: Double, arcAngle: Double, rotation: Double = 0) { let extent = CGFloat(startAngle) let end = extent + CGFloat(arcAngle) let cx = CGFloat(x + w / 2) let cy = CGFloat(y + h / 2) if w == h && rotation == 0 { bezierPath.addArc(withCenter: CGPoint(x: cx, y: cy), radius: CGFloat(w / 2), startAngle: extent, endAngle: end, clockwise: arcAngle >= 0) } else { let maxSize = CGFloat(max(w, h)) let path = MBezierPath(arcCenter: CGPoint.zero, radius: maxSize / 2, startAngle: extent, endAngle: end, clockwise: arcAngle >= 0) #if os(iOS) var transform = CGAffineTransform(translationX: cx, y: cy) transform = transform.rotated(by: CGFloat(rotation)) path.apply(transform.scaledBy(x: CGFloat(w) / maxSize, y: CGFloat(h) / maxSize)) #elseif os(OSX) var transform = AffineTransform(translationByX: cx, byY: cy) transform.rotate(byDegrees: CGFloat(rotation)) path.transform(using: transform) #endif bezierPath.append(path) } } func e(_ x: Double, y: Double, w: Double, h: Double, startAngle: Double, arcAngle: Double) { // TODO: only circle now if let cur = currentPoint { E(x + Double(cur.x), y: y + Double(cur.y), w: w, h: h, startAngle: startAngle, arcAngle: arcAngle) } } func Z() { if let initPoint = initialPoint { lineTo(initPoint) } bezierPath.close() } func setQuadrPoint(_ p: CGPoint, quadr: CGPoint) { currentPoint = p quadrPoint = quadr cubicPoint = nil } func setCubicPoint(_ p: CGPoint, cubic: CGPoint) { currentPoint = p cubicPoint = cubic quadrPoint = nil } func setInitPoint(_ p: CGPoint) { setPoint(p) initialPoint = p } func setPoint(_ p: CGPoint) { currentPoint = p cubicPoint = nil quadrPoint = nil } // TODO: think about this for part in path.segments { var data = part.data switch part.type { case .M: M(data[0], y: data[1]) data.removeSubrange(Range(uncheckedBounds: (lower: 0, upper: 2))) while data.count >= 2 { L(data[0], y: data[1]) data.removeSubrange((0 ..< 2)) } case .m: m(data[0], y: data[1]) data.removeSubrange((0 ..< 2)) while data.count >= 2 { l(data[0], y: data[1]) data.removeSubrange((0 ..< 2)) } case .L: while data.count >= 2 { L(data[0], y: data[1]) data.removeSubrange((0 ..< 2)) } case .l: while data.count >= 2 { l(data[0], y: data[1]) data.removeSubrange((0 ..< 2)) } case .H: H(data[0]) case .h: h(data[0]) case .V: V(data[0]) case .v: v(data[0]) case .C: while data.count >= 6 { C(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5]) data.removeSubrange((0 ..< 6)) } case .c: while data.count >= 6 { c(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5]) data.removeSubrange((0 ..< 6)) } case .S: while data.count >= 4 { S(data[0], y2: data[1], x: data[2], y: data[3]) data.removeSubrange((0 ..< 4)) } case .s: while data.count >= 4 { s(data[0], y2: data[1], x: data[2], y: data[3]) data.removeSubrange((0 ..< 4)) } case .Q: Q(data[0], y1: data[1], x: data[2], y: data[3]) case .q: q(data[0], y1: data[1], x: data[2], y: data[3]) case .T: T(data[0], y: data[1]) case .t: t(data[0], y: data[1]) case .A: A(data[0], ry: data[1], angle: data[2], largeArc: num2bool(data[3]), sweep: num2bool(data[4]), x: data[5], y: data[6]) case .a: a(data[0], ry: data[1], angle: data[2], largeArc: num2bool(data[3]), sweep: num2bool(data[4]), x: data[5], y: data[6]) case .E: E(data[0], y: data[1], w: data[2], h: data[3], startAngle: data[4], arcAngle: data[5]) case .e: e(data[0], y: data[1], w: data[2], h: data[3], startAngle: data[4], arcAngle: data[5]) case .z: Z() } } return bezierPath } class func calcAngle(ux: Double, uy: Double, vx: Double, vy: Double) -> Double { let sign = copysign(1, ux * vy - uy * vx) let value = (ux * vx + uy * vy) / (sqrt(ux * ux + uy * uy) * sqrt(vx * vx + vy * vy)) if value < -1 { return sign * .pi } else if value > 1 { return 0 } else { return sign * acos(value) } } class func num2bool(_ double: Double) -> Bool { return double > 0.5 ? true : false } internal class func setStrokeAttributes(_ stroke: Stroke, ctx: CGContext?) { ctx!.setLineWidth(CGFloat(stroke.width)) ctx!.setLineJoin(stroke.join.toCG()) ctx!.setLineCap(stroke.cap.toCG()) ctx!.setMiterLimit(CGFloat(stroke.miterLimit)) if !stroke.dashes.isEmpty { ctx?.setLineDash(phase: CGFloat(stroke.offset), lengths: stroke.dashes.map { CGFloat($0) }) } } internal class func setGeometry(_ locus: Locus, ctx: CGContext) { if let rect = locus as? Rect { ctx.addRect(rect.toCG()) } else if let round = locus as? RoundRect { let corners = CGSize(width: CGFloat(round.rx), height: CGFloat(round.ry)) let path = MBezierPath(roundedRect: round.rect.toCG(), byRoundingCorners: MRectCorner.allCorners, cornerRadii: corners).cgPath ctx.addPath(path) } else if let circle = locus as? Circle { let cx = circle.cx let cy = circle.cy let r = circle.r ctx.addEllipse(in: CGRect(x: cx - r, y: cy - r, width: r * 2, height: r * 2)) } else if let ellipse = locus as? Ellipse { let cx = ellipse.cx let cy = ellipse.cy let rx = ellipse.rx let ry = ellipse.ry ctx.addEllipse(in: CGRect(x: cx - rx, y: cy - ry, width: rx * 2, height: ry * 2)) } else { ctx.addPath(locus.toCGPath()) } } internal class func setClip(_ clip: Locus?, ctx: CGContext) { if let rect = clip as? Rect { ctx.clip(to: CGRect(x: rect.x, y: rect.y, width: rect.w, height: rect.h)) } else if let clip = clip { RenderUtils.toBezierPath(clip).addClip() } } }