diff --git a/Sources/CollectionsBenchmark/Basics/Measurement.swift b/Sources/CollectionsBenchmark/Basics/Measurement.swift new file mode 100644 index 0000000..72d938d --- /dev/null +++ b/Sources/CollectionsBenchmark/Basics/Measurement.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// A point in the document coordinate system, i.e., a pair of size and +/// time values. +public struct Measurement: Hashable, Codable { + public let size: Size + public let time: Time + + public init(size: Size, time: Time) { + self.size = size + self.time = time + } +} diff --git a/Sources/CollectionsBenchmark/Basics/TaskResults.swift b/Sources/CollectionsBenchmark/Basics/TaskResults.swift index 91ecda1..1a2c4fe 100644 --- a/Sources/CollectionsBenchmark/Basics/TaskResults.swift +++ b/Sources/CollectionsBenchmark/Basics/TaskResults.swift @@ -167,14 +167,14 @@ extension TaskResults { for statistic: Sample.Statistic, percentile: Double, amortizedTime: Bool - ) -> Curve { - var curve = Curve() + ) -> Curve { + var curve = Curve() for (size, sample) in _samples { let sample = sample.discardingPercentile(above: percentile) guard let time = sample[statistic] ?? sample[.mean] else { continue } let t = amortizedTime ? time.amortized(over: size) : time - curve.points.append(BenchmarkResults.Point(size: size, time: t)) + curve.points.append(Measurement(size: size, time: t)) } return curve } diff --git a/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Library+Render.swift b/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Library+Render.swift index 452a510..2d81369 100644 --- a/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Library+Render.swift +++ b/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Library+Render.swift @@ -115,7 +115,7 @@ extension _BenchmarkCLI.Library { let data = try renderer.render( graphics, format: format.rawValue, - bitmapScale: CGFloat(options.scale)) + bitmapScale: options.scale) try data.write(to: url) count += 1 } diff --git a/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Render+Options.swift b/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Render+Options.swift index dc8380d..1697732 100644 --- a/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Render+Options.swift +++ b/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Render+Options.swift @@ -9,7 +9,6 @@ // //===----------------------------------------------------------------------===// -import Foundation // CGRect import ArgumentParser extension _BenchmarkCLI.Render { @@ -85,8 +84,8 @@ extension _BenchmarkCLI.Render { return options } - var _bounds: CGRect { - CGRect(x: 0, y: 0, width: width, height: height) + var _bounds: Rectangle { + Rectangle(x: 0, y: 0, width: width, height: height) } } } diff --git a/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Render.swift b/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Render.swift index d6f7991..7b090b2 100644 --- a/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Render.swift +++ b/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Render.swift @@ -56,13 +56,13 @@ extension _BenchmarkCLI { in: results, options: try options.chartOptions()) let graphics = chart.draw( - bounds: CGRect(x: 0, y: 0, width: options.width, height: options.height), + bounds: Rectangle(x: 0, y: 0, width: options.width, height: options.height), theme: theme, renderer: renderer) let data = try renderer.render( graphics, format: output.pathExtension, - bitmapScale: CGFloat(options.scale)) + bitmapScale: options.scale) try data.write(to: output, options: .atomic) } } diff --git a/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Results+Compare.swift b/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Results+Compare.swift index 19948c2..e762562 100644 --- a/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Results+Compare.swift +++ b/Sources/CollectionsBenchmark/BenchmarkCLI/BenchmarkCLI+Results+Compare.swift @@ -194,7 +194,7 @@ extension _BenchmarkCLI.Results { let data = try renderer.render( graphics, format: format.rawValue, - bitmapScale: CGFloat(chartOptions.scale)) + bitmapScale: chartOptions.scale) let filename = self.filename(title: title, index: i, format: format) let url = dir.appendingPathComponent(filename) try data.write(to: url, options: .atomic) diff --git a/Sources/CollectionsBenchmark/Charts/Band.swift b/Sources/CollectionsBenchmark/Charts/Band.swift index 6e04a0f..cf112a0 100644 --- a/Sources/CollectionsBenchmark/Charts/Band.swift +++ b/Sources/CollectionsBenchmark/Charts/Band.swift @@ -90,7 +90,7 @@ extension Band { } } -extension Band where Element == Curve { +extension Band where Element == Curve { var sizeRange: ClosedRange? { _union(bottom.sizeRange, _union(center.sizeRange, top.sizeRange)) } diff --git a/Sources/CollectionsBenchmark/Charts/Chart.swift b/Sources/CollectionsBenchmark/Charts/Chart.swift index bc22515..f72f2b1 100644 --- a/Sources/CollectionsBenchmark/Charts/Chart.swift +++ b/Sources/CollectionsBenchmark/Charts/Chart.swift @@ -13,13 +13,11 @@ import Foundation /// The data for a benchmark chart in a nice preprocessed format. public struct Chart { - public typealias Point = BenchmarkResults.Point - public let options: Options public let sizeScale: ChartScale public let timeScale: ChartScale public let links: [TaskID: URL] - internal let _data: _SimpleOrderedDictionary>> + internal let _data: _SimpleOrderedDictionary>> init( taskIDs: [TaskID], @@ -35,7 +33,7 @@ public struct Chart { // Gather data in the results coordinate system. self._data = .init( uniqueKeysWithValues: taskIDs.lazy.map { id in - var band = Band>(.init()) + var band = Band>(.init()) for bi in BandIndex.allCases { let statistic = options.statistics[bi] guard statistic != .none else { continue } @@ -85,14 +83,14 @@ extension Chart { _data.map { $0.key } } - public func chartData(for taskID: TaskID) -> Band>? { + public func chartData(for taskID: TaskID) -> Band>? { _data[taskID] } } extension Chart { public func draw( - bounds: CGRect, + bounds: Rectangle, theme: Theme, renderer: Renderer ) -> Graphics { @@ -108,21 +106,21 @@ extension Chart { if !caption.isEmpty { let metrics = renderer.measure(theme.axisLabels.font, caption) let division = rect.divided( - atDistance: metrics.size.height + theme.axisLeading, - from: .maxYEdge) + atDistance: metrics.height + theme.axisLeading, + from: .maxY) rect = division.remainder let captionRect = division.slice - .inset(by: EdgeInsets(left: axisMetrics.size.width)) - .divided(atDistance: metrics.size.width, from: .minXEdge) + .inset(by: EdgeInsets(minX: axisMetrics.width)) + .divided(atDistance: metrics.width, from: .minX) .slice g.addText(caption, style: theme.axisLabels, - in: CGRect( + in: Rectangle( x: captionRect.minX, - y: captionRect.maxY - metrics.size.height, - width: metrics.size.width, - height: metrics.size.height), + y: captionRect.maxY - metrics.height, + width: metrics.width, + height: metrics.height), descender: metrics.descender) // Render hallmark. @@ -131,26 +129,26 @@ extension Chart { g.addText(hallmark, style: theme.axisLabels, linkTarget: URL(string: _projectURL)!, - in: CGRect( + in: Rectangle( x: max(captionRect.maxX + theme.xPadding, - division.slice.maxX - axisMetrics.size.width - hmMetrics.size.width), - y: captionRect.maxY - hmMetrics.size.height, - width: hmMetrics.size.width, - height: hmMetrics.size.height), + division.slice.maxX - axisMetrics.width - hmMetrics.width), + y: captionRect.maxY - hmMetrics.height, + width: hmMetrics.width, + height: hmMetrics.height), descender: hmMetrics.descender) } // Allocate space for axis labels. rect = rect.inset( by: EdgeInsets( - left: axisMetrics.size.width, - bottom: axisMetrics.size.height + theme.axisLeading, - right: axisMetrics.size.width)) + minX: axisMetrics.width, + maxX: axisMetrics.width, + maxY: axisMetrics.height + theme.axisLeading)) let chartBounds = rect - var chartTransform = AffineTransform.identity - chartTransform.translate(x: rect.minX, y: rect.minY) - chartTransform.scale(x: rect.width, y: rect.height) + let chartTransform = Transform.identity + .translated(x: rect.minX, y: rect.minY) + .scaled(x: rect.width, y: rect.height) _renderGridlinesForSizeAxis( chartBounds: chartBounds, @@ -177,10 +175,11 @@ extension Chart { // Convert curve data into chart coordinates. let bands = _data.map { item in item.value.map { curve in - curve.map { point -> CGPoint in - let p = CGPoint(x: sizeScale.position(for: Double(point.size.rawValue)), - y: 1 - timeScale.position(for: point.time.seconds)) - return chartTransform.transform(p) + curve.map { point -> Point in + Point( + x: sizeScale.position(for: Double(point.size.rawValue)), + y: 1 - timeScale.position(for: point.time.seconds)) + .applying(chartTransform) } } } @@ -224,7 +223,7 @@ extension Chart { extension Chart { internal func _renderGridlinesForSizeAxis( - chartBounds: CGRect, + chartBounds: Rectangle, in g: inout Graphics, theme: Theme, renderer: Renderer @@ -233,15 +232,17 @@ extension Chart { var lines: [Line] = sizeScale.gridlines.map { gridline in let xMid = chartBounds.minX + gridline.position * chartBounds.width let yTop = chartBounds.maxY + 3 - let start = CGPoint(x: xMid, y: chartBounds.minY) - let end = CGPoint(x: xMid, y: chartBounds.maxY) + let start = Point(x: xMid, y: chartBounds.minY) + let end = Point(x: xMid, y: chartBounds.maxY) let line = Shape( path: .line(from: start, to: end), stroke: gridline.kind == .major ? theme.majorGridline : theme.minorGridline) let label: Text? = gridline.label.map { label in let metrics = renderer.measure(theme.axisLabels.font, label) - let pos = CGPoint(x: xMid - metrics.size.width / 2, y: yTop) - let box = CGRect(origin: pos, size: metrics.size) + let pos = Point(x: xMid - metrics.width / 2, y: yTop) + let box = Rectangle( + x: pos.x, y: pos.y, + width: metrics.width, height: metrics.height) return Text(label, style: theme.axisLabels, in: box, descender: metrics.descender) } @@ -249,10 +250,10 @@ extension Chart { } // Returns true if there isn't enough space to display all labels. func needsThinning(_ lines: [Line]) -> Bool { - var previousFrame: CGRect = .null + var previousFrame: Rectangle = .null for line in lines { guard let label = line.label else { continue } - let enlarged = label.boundingBox.insetBy(dx: -3, dy: 0) + let enlarged = label.boundingBox.inset(dx: -3, dy: 0) if previousFrame.intersects(enlarged) { return true } previousFrame = enlarged } @@ -280,33 +281,33 @@ extension Chart { extension Chart { internal func _renderGridlinesForTimeAxis( - chartBounds: CGRect, + chartBounds: Rectangle, in g: inout Graphics, theme: Theme, renderer: Renderer ) { let suppressMinorLines = (!options.amortizedTime || chartBounds.height < 200) - var previousLabelBox = CGRect.null + var previousLabelBox = Rectangle.null for gridline in timeScale.gridlines { guard gridline.kind == .major || !suppressMinorLines else { continue } let y = chartBounds.maxY - gridline.position * chartBounds.height g.addLine( - from: CGPoint(x: chartBounds.minX, y: y), - to: CGPoint(x: chartBounds.maxX, y: y), + from: Point(x: chartBounds.minX, y: y), + to: Point(x: chartBounds.maxX, y: y), stroke: gridline.kind == .major ? theme.majorGridline : theme.minorGridline) if gridline.kind == .major, let label = gridline.label { let metrics = renderer.measure(theme.axisLabels.font, label) - let yMid = y - metrics.size.height / 6 - let left = CGRect( - x: chartBounds.minX - theme.xPadding - metrics.size.width, - y: yMid - metrics.size.height / 2, - width: metrics.size.width, - height: metrics.size.height) - let right = CGRect( + let yMid = y - metrics.height / 6 + let left = Rectangle( + x: chartBounds.minX - theme.xPadding - metrics.width, + y: yMid - metrics.height / 2, + width: metrics.width, + height: metrics.height) + let right = Rectangle( x: chartBounds.maxX + theme.xPadding, - y: yMid - metrics.size.height / 2, - width: metrics.size.width, - height: metrics.size.height) + y: yMid - metrics.height / 2, + width: metrics.width, + height: metrics.height) if left.intersects(previousLabelBox) { continue } previousLabelBox = left g.addText(label, style: theme.axisLabels, @@ -320,12 +321,12 @@ extension Chart { extension Chart { struct _LegendItem { - var start: CGPoint - var end: CGPoint + var start: Point + var end: Point var strokes: [Stroke] var label: Text - mutating func _offset(by delta: CGPoint) { + mutating func _offset(by delta: Point) { start.x += delta.x start.y += delta.y end.x += delta.x @@ -336,64 +337,64 @@ extension Chart { } internal func _layoutLegend( - chartBounds: CGRect, + chartBounds: Rectangle, options: Options, theme: Theme, renderer: Renderer - ) -> (box: CGRect, items: [_LegendItem])? { + ) -> (box: Rectangle, items: [_LegendItem])? { guard theme.legendPosition != .hidden else { return nil } - typealias Metrics = (size: CGSize, descender: CGFloat) + typealias Metrics = (width: Double, height: Double, descender: Double) let labels: [(taskID: TaskID, string: String, metrics: Metrics)] = _data.map { item in let label = item.key.typesetDescription let metrics = renderer.measure(theme.legendLabels.font, label) return (item.key, label, metrics) } - let maxHeight: CGFloat = labels.reduce(0) { - Swift.max($0, $1.metrics.size.height) + let maxHeight: Double = labels.reduce(0) { + Swift.max($0, $1.metrics.height) } - let maxWidth: CGFloat = labels.reduce(0) { - Swift.max($0, $1.metrics.size.width) + let maxWidth: Double = labels.reduce(0) { + Swift.max($0, $1.metrics.width) } - var y = theme.legendPadding.top + var y = theme.legendPadding.minY var items: [_LegendItem] = labels.enumerated().map { (index, label) in let metrics = label.metrics if index > 0 { y += theme.legendLineLeading } - var x = theme.legendPadding.left - let sampleRect = CGRect( + var x = theme.legendPadding.minX + let sampleRect = Rectangle( x: x, - y: y + maxHeight - metrics.size.height, + y: y + maxHeight - metrics.height, width: theme.legendLineSampleWidth, - height: metrics.size.height) + height: metrics.height) x += sampleRect.width + theme.legendSeparation let text = Text( label.string, style: theme.legendLabels, linkTarget: self.links[label.taskID], - in: CGRect(origin: CGPoint(x: x, y: y), size: metrics.size), + in: Rectangle(x: x, y: y, width: metrics.width, height: metrics.height), descender: metrics.descender) let curveTheme = theme._themeForCurve(index: index, of: labels.count) - let lineRect = sampleRect.insetBy(dx: curveTheme.lineWidth / 2, dy: 0) + let lineRect = sampleRect.inset(dx: curveTheme.lineWidth / 2, dy: 0) let item = _LegendItem( - start: CGPoint(x: lineRect.minX, y: lineRect.midY), - end: CGPoint(x: lineRect.maxX, y: lineRect.midY), + start: Point(x: lineRect.minX, y: lineRect.midY), + end: Point(x: lineRect.maxX, y: lineRect.midY), strokes: [curveTheme.stroke, theme.hairlines], label: text) y += maxHeight return item } - y += theme.legendPadding.bottom + y += theme.legendPadding.maxY - let size = CGSize( - width: theme.legendPadding.left + theme.legendLineSampleWidth - + theme.legendSeparation + maxWidth + theme.legendPadding.right, - height: y) + let size = Vector( + dx: theme.legendPadding.minY + theme.legendLineSampleWidth + + theme.legendSeparation + maxWidth + theme.legendPadding.maxY, + dy: y) let box = theme._legendFrame(for: size, in: chartBounds) for i in items.indices { items[i]._offset(by: box.origin) diff --git a/Sources/CollectionsBenchmark/Charts/ChartScales.swift b/Sources/CollectionsBenchmark/Charts/ChartScales.swift index f98a5a2..1c71c4f 100644 --- a/Sources/CollectionsBenchmark/Charts/ChartScales.swift +++ b/Sources/CollectionsBenchmark/Charts/ChartScales.swift @@ -9,8 +9,6 @@ // //===----------------------------------------------------------------------===// -import Foundation // CGPoint, CGFloat - extension Chart { public struct Gridline { public enum Kind { @@ -18,10 +16,10 @@ extension Chart { case minor } public let kind: Kind - public let position: CGFloat + public let position: Double public let label: String? - public init(_ kind: Kind, position: CGFloat, label: String? = nil) { + public init(_ kind: Kind, position: Double, label: String? = nil) { self.kind = kind self.position = position self.label = label @@ -31,7 +29,7 @@ extension Chart { public protocol ChartScale { // Convert the given value to chart coordinates using this scale. - func position(for value: Double) -> CGFloat + func position(for value: Double) -> Double /// The range of values that can be displayed using this scale. var displayedRange: ClosedRange { get } @@ -45,7 +43,7 @@ extension Chart { public struct EmptyScale: ChartScale { public let displayedRange: ClosedRange = 0...1 public let gridlines: [Gridline] = [] - public func position(for value: Double) -> CGFloat { CGFloat(2) } + public func position(for value: Double) -> Double { 2 } } public struct LogarithmicScale: ChartScale { @@ -108,20 +106,20 @@ extension Chart { self.displayedRange = min ... max self._exponentRange = minExponent ... maxExponent - self._a = log2(min) - self._b = log2(max) - log2(min) + self._a = _log2(min) + self._b = _log2(max) - _log2(min) } - public func position(for value: Double) -> CGFloat { + public func position(for value: Double) -> Double { if value <= 0 { return 0 } - return CGFloat((log2(value) - _a) / _b) + return (_log2(value) - _a) / _b } public var gridlines: [Gridline] { var gridlines: [Gridline] = [] let step = _isDecimal ? 10.0 : 2.0 for exponent in _exponentRange { - let position = self.position(for: pow(step, Double(exponent))) + let position = self.position(for: _pow(step, Double(exponent))) let label = _labeler(exponent) let line = Gridline(.major, position: position, label: label) gridlines.append(line) @@ -172,15 +170,15 @@ extension Chart { stepSize *= steps.next() } } - let min = stepSize * floor(range.lowerBound / stepSize) - let max = stepSize * ceil(range.upperBound / stepSize) + let min = stepSize * (range.lowerBound / stepSize).rounded(.down) + let max = stepSize * (range.upperBound / stepSize).rounded(.up) self.displayedRange = min ... max self._stepSize = stepSize } - public func position(for value: Double) -> CGFloat { + public func position(for value: Double) -> Double { let denom = displayedRange.upperBound - displayedRange.lowerBound - return CGFloat((value - displayedRange.lowerBound) / denom) + return (value - displayedRange.lowerBound) / denom } public var gridlines: [Gridline] { diff --git a/Sources/CollectionsBenchmark/Charts/Curve.swift b/Sources/CollectionsBenchmark/Charts/Curve.swift index dc4c234..1635236 100644 --- a/Sources/CollectionsBenchmark/Charts/Curve.swift +++ b/Sources/CollectionsBenchmark/Charts/Curve.swift @@ -43,7 +43,7 @@ extension Curve { } } -extension Curve where Point == BenchmarkResults.Point { +extension Curve where Point == Measurement { var sizeRange: ClosedRange? { guard !points.isEmpty else { return nil } let min = points.min(by: { $0.size < $1.size }) @@ -60,22 +60,10 @@ extension Curve where Point == BenchmarkResults.Point { } extension BenchmarkResults { - /// A point in the document coordinate system, i.e., a pair of size and - /// time values. - public struct Point: Hashable, Codable { - public let size: Size - public let time: Time - - public init(size: Size, time: Time) { - self.size = size - self.time = time - } - } - - public func curve(id: TaskID, statistic: Sample.Statistic) -> Curve { - let points: [Point] = self[id: id].compactMap { (size, sample) in + public func curve(id: TaskID, statistic: Sample.Statistic) -> Curve { + let points: [Measurement] = self[id: id].compactMap { (size, sample) in guard let time = sample[statistic] else { return nil } - return Point(size: size, time: time) + return Measurement(size: size, time: time) } return Curve(points) } diff --git a/Sources/CollectionsBenchmark/Charts/Graphics/EdgeInsets.swift b/Sources/CollectionsBenchmark/Charts/Geometry/EdgeInsets.swift similarity index 51% rename from Sources/CollectionsBenchmark/Charts/Graphics/EdgeInsets.swift rename to Sources/CollectionsBenchmark/Charts/Geometry/EdgeInsets.swift index 3f28378..3407ed2 100644 --- a/Sources/CollectionsBenchmark/Charts/Graphics/EdgeInsets.swift +++ b/Sources/CollectionsBenchmark/Charts/Geometry/EdgeInsets.swift @@ -12,37 +12,37 @@ import Foundation public struct EdgeInsets: Hashable, Codable { - public var top: CGFloat - public var left: CGFloat - public var bottom: CGFloat - public var right: CGFloat - - public init() { - top = 0 - left = 0 - bottom = 0 - right = 0 - } - + public var top: Double + public var left: Double + public var bottom: Double + public var right: Double + public init( - top: CGFloat = 0, - left: CGFloat = 0, - bottom: CGFloat = 0, - right: CGFloat = 0 + top: Double = 0, + left: Double = 0, + bottom: Double = 0, + right: Double = 0 ) { self.top = top self.left = left self.bottom = bottom self.right = right } -} -extension CGRect { - public func inset(by insets: EdgeInsets) -> CGRect { - CGRect( - x: self.origin.x + insets.left, - y: self.origin.y + insets.top, - width: self.size.width - insets.left - insets.right, - height: self.size.height - insets.bottom - insets.top) + public init( + minX: Double = 0, + minY: Double = 0, + maxX: Double = 0, + maxY: Double = 0 + ) { + self.init(top: minY, left: minX, bottom: maxY, right: maxX) } + + public var minX: Double { left } + public var minY: Double { top } + public var maxX: Double { right } + public var maxY: Double { bottom } + + public var dx: Double { minX + maxX } + public var dy: Double { minY + maxY } } diff --git a/Sources/CollectionsBenchmark/Charts/Graphics/Path.swift b/Sources/CollectionsBenchmark/Charts/Geometry/Path.swift similarity index 81% rename from Sources/CollectionsBenchmark/Charts/Graphics/Path.swift rename to Sources/CollectionsBenchmark/Charts/Geometry/Path.swift index 20a49ff..691ae98 100644 --- a/Sources/CollectionsBenchmark/Charts/Graphics/Path.swift +++ b/Sources/CollectionsBenchmark/Charts/Geometry/Path.swift @@ -9,15 +9,13 @@ // //===----------------------------------------------------------------------===// -import Foundation // For CGPoint, CGRect - public enum Path { /// A line segment between two points. - case line(from: CGPoint, to: CGPoint) + case line(from: Point, to: Point) /// A rectangle. - case rect(CGRect) + case rect(Rectangle) /// A series of connected line segments. - case lines([CGPoint]) + case lines([Point]) } extension Path: Codable { @@ -27,12 +25,12 @@ extension Path: Codable { switch kind { case "line": self = .line( - from: try container.decode(CGPoint.self), - to: try container.decode(CGPoint.self)) + from: try container.decode(Point.self), + to: try container.decode(Point.self)) case "rect": - self = .rect(try container.decode(CGRect.self)) + self = .rect(try container.decode(Rectangle.self)) case "lines": - self = .lines(try container.decode([CGPoint].self)) + self = .lines(try container.decode([Point].self)) default: throw DecodingError.dataCorruptedError( in: container, diff --git a/Sources/CollectionsBenchmark/Charts/Geometry/Point.swift b/Sources/CollectionsBenchmark/Charts/Geometry/Point.swift new file mode 100644 index 0000000..f110622 --- /dev/null +++ b/Sources/CollectionsBenchmark/Charts/Geometry/Point.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +@frozen +public struct Point { + public var x: Double + public var y: Double + + @inlinable + public init(x: Double, y: Double) { + self.x = x + self.y = y + } + + @inlinable + public static var zero: Point { Point(x: 0, y: 0) } +} + +extension Point: Hashable { + @inlinable + public static func ==(left: Self, right: Self) -> Bool { + left.x == right.x && left.y == right.y + } + + @inlinable + public func hash(into hasher: inout Hasher) { + hasher.combine(x) + hasher.combine(y) + } +} + +extension Point: Codable { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let x = try container.decode(Double.self) + let y = try container.decode(Double.self) + self.init(x: x, y: y) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(x) + try container.encode(y) + } +} + +extension Point { + @inlinable + public func applying(_ t: Transform) -> Point { + Point( + x: t.a * x + t.c * y + t.tx, + y: t.b * x + t.d * y + t.ty) + } +} diff --git a/Sources/CollectionsBenchmark/Charts/Geometry/Rectangle.swift b/Sources/CollectionsBenchmark/Charts/Geometry/Rectangle.swift new file mode 100644 index 0000000..4b8aaac --- /dev/null +++ b/Sources/CollectionsBenchmark/Charts/Geometry/Rectangle.swift @@ -0,0 +1,184 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +@frozen +public struct Rectangle { + public var origin: Point + public var size: Vector + + @inlinable + public init(origin: Point, size: Vector) { + self.origin = origin + self.size = size + } + + @inlinable + public init(x: Double, y: Double, width: Double, height: Double) { + self.origin = Point(x: x, y: y) + self.size = Vector(dx: width, dy: height) + } + + @inlinable + public init(x: N, y: N, width: N, height: N) { + self.init( + x: Double(x), + y: Double(y), + width: Double(width), + height: Double(height)) + } + + @inlinable + public init(x: N, y: N, width: N, height: N) { + self.init( + x: Double(x), + y: Double(y), + width: Double(width), + height: Double(height)) + } + + public static var null: Rectangle { + Rectangle(x: .infinity, y: .infinity, width: 0, height: 0) + } + + public var minX: Double { origin.x + min(0, size.dx) } + public var maxX: Double { origin.x + max(0, size.dx) } + public var midX: Double { 0.5 * (minX + maxX) } + + public var minY: Double { origin.y + min(0, size.dy) } + public var maxY: Double { origin.y + max(0, size.dy) } + public var midY: Double { 0.5 * (minY + maxY) } + + @inlinable public var width: Double { abs(size.dx) } + @inlinable public var height: Double { abs(size.dy) } +} + +extension Rectangle: Hashable { + public static func ==(left: Self, right: Self) -> Bool { + let r1 = left.standardized + let r2 = right.standardized + return r1.origin == r2.origin && r1.size == r2.size + } + + public func hash(into hasher: inout Hasher) { + let r = self.standardized + hasher.combine(r.origin) + hasher.combine(r.size) + } +} + +extension Rectangle: Codable { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let origin = try container.decode(Point.self) + let size = try container.decode(Vector.self) + self.init(origin: origin, size: size) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(origin) + try container.encode(size) + } +} + +extension Rectangle { + public var isNull: Bool { + (origin.x.isInfinite && origin.x.sign == .plus) + || (origin.y.isInfinite && origin.y.sign == .plus) + } + + public func intersects(_ other: Rectangle) -> Bool { + let r1 = self.standardized + let r2 = other.standardized + guard !r1.isNull && !r2.isNull else { return false } + + let minX1 = r1.origin.x + let minX2 = r2.origin.x + if minX1 < minX2, r1.origin.x + r1.size.dx <= minX2 { return false } + if minX1 > minX2, r2.origin.x + r2.size.dx <= minX1 { return false } + + let minY1 = r1.origin.y + let minY2 = r2.origin.y + if minY1 < minY2, r1.origin.y + r1.size.dy <= minY2 { return false } + if minY1 > minY2, r2.origin.y + r2.size.dy <= minY1 { return false } + return true + } +} + +extension Rectangle { + public func inset(by insets: EdgeInsets) -> Rectangle { + let r = Rectangle( + x: self.minX + insets.minX, + y: self.minY + insets.minY, + width: self.width - insets.dx, + height: self.height - insets.dy) + guard r.size.dx >= 0 && r.size.dy >= 0 else { return .null } + return r + } + + public func inset(dx: Double, dy: Double) -> Rectangle { + inset(by: EdgeInsets(minX: dx, minY: dy, maxX: dx, maxY: dy)) + } + + public var standardized: Rectangle { + Rectangle(x: minX, y: minY, width: width, height: height) + } + + public var integral: Rectangle { + Rectangle( + x: minX.rounded(.down), + y: minY.rounded(.down), + width: width.rounded(.up), + height: height.rounded(.up)) + } + + public enum Edge { + case minX + case maxX + case minY + case maxY + } + + public func divided( + atDistance distance: Double, + from edge: Edge + ) -> (slice: Rectangle, remainder: Rectangle) { + let r = self.standardized + guard !r.isNull else { return (.null, .null) } + let split: Double + switch edge { + case .minX: + split = min(max(distance, 0), r.size.dx) + case .maxX: + split = r.size.dx - min(max(distance, 0), r.size.dx) + case .minY: + split = min(max(distance, 0), r.size.dy) + case .maxY: + split = r.size.dy - min(max(distance, 0), r.size.dy) + } + + switch edge { + case .minX, .maxX: + let r1 = Rectangle(x: r.origin.x, y: r.origin.y, + width: split, height: r.size.dy) + let r2 = Rectangle(x: r.origin.x + split, y: r.origin.y, + width: r.size.dx - split, height: r.size.dy) + return (edge == .minX ? (r1, r2) : (r2, r1)) + case .minY, .maxY: + let r1 = Rectangle(x: r.origin.x, y: r.origin.y, + width: r.size.dx, height: split) + let r2 = Rectangle(x: r.origin.x, y: r.origin.y + split, + width: r.size.dx, height: r.size.dy - split) + return (edge == .minY ? (r1, r2) : (r2, r1)) + } + } +} + diff --git a/Sources/CollectionsBenchmark/Charts/Geometry/Transform.swift b/Sources/CollectionsBenchmark/Charts/Geometry/Transform.swift new file mode 100644 index 0000000..854e0ef --- /dev/null +++ b/Sources/CollectionsBenchmark/Charts/Geometry/Transform.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// A 2D affine transformation represented by a 3x3 matrix: +/// +/// a b 0 +/// c d 0 +/// tx ty 1 +@frozen +public struct Transform { + public var a: Double + public var b: Double + public var c: Double + public var d: Double + public var tx: Double + public var ty: Double + + @inlinable + public init( + a: Double, + b: Double, + c: Double, + d: Double, + tx: Double, + ty: Double + ) { + self.a = a + self.b = b + self.c = c + self.d = d + self.tx = tx + self.ty = ty + } +} + +extension Transform: Hashable { + public static func ==(left: Self, right: Self) -> Bool { + left.a == right.a + && left.b == right.b + && left.c == right.c + && left.d == right.d + && left.tx == right.tx + && left.ty == right.ty + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(a) + hasher.combine(b) + hasher.combine(c) + hasher.combine(d) + hasher.combine(tx) + hasher.combine(ty) + } +} + +extension Transform: Codable { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let a = try container.decode(Double.self) + let b = try container.decode(Double.self) + let c = try container.decode(Double.self) + let d = try container.decode(Double.self) + let tx = try container.decode(Double.self) + let ty = try container.decode(Double.self) + self.init(a: a, b: b, c: c, d: d, tx: tx, ty: ty) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(a) + try container.encode(b) + try container.encode(c) + try container.encode(d) + try container.encode(tx) + try container.encode(ty) + } +} + +extension Transform { + @inlinable + public static var identity: Transform { + Transform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0) + } + + public func concatenating(_ other: Transform) -> Transform { + Transform( + a: self.a * other.a + self.b * other.c, + b: self.a * other.b + self.b * other.d, + c: self.c * other.a + self.d * other.c, + d: self.c * other.b + self.d * other.d, + tx: self.tx * other.a + self.ty * other.c + other.tx, + ty: self.tx * other.b + self.ty * other.d + other.ty) + } + + public func scaled(_ scale: Double) -> Transform { + Transform( + a: scale * a, + b: scale * b, + c: scale * c, + d: scale * d, + tx: tx, + ty: ty) + } + + public func scaled(x: Double, y: Double) -> Transform { + Transform( + a: x * a, + b: x * b, + c: y * c, + d: y * d, + tx: tx, + ty: ty) + } + + public func translated(x: Double, y: Double) -> Transform { + Transform( + a: a, + b: b, + c: c, + d: d, + tx: tx + x * a + y * c, + ty: ty + x * b + y * d) + } + + public func rotated(_ radians: Double) -> Transform { + let cosine = _cos(radians) + let sine = _sin(radians) + let rotation = Transform(a: cosine, b: sine, c: -sine, d: cosine, tx: 0, ty: 0) + return self.concatenating(rotation) + } +} diff --git a/Sources/CollectionsBenchmark/Charts/Geometry/Vector.swift b/Sources/CollectionsBenchmark/Charts/Geometry/Vector.swift new file mode 100644 index 0000000..786f79e --- /dev/null +++ b/Sources/CollectionsBenchmark/Charts/Geometry/Vector.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +@frozen +public struct Vector { + public var dx: Double + public var dy: Double + + @inlinable + public init(dx: Double, dy: Double) { + self.dx = dx + self.dy = dy + } + + @inlinable + public static var zero: Vector { Vector(dx: 0, dy: 0) } +} + +extension Vector: Hashable { + @inlinable + public static func ==(left: Self, right: Self) -> Bool { + left.dx == right.dx && left.dy == right.dy + } + + @inlinable + public func hash(into hasher: inout Hasher) { + hasher.combine(dx) + hasher.combine(dy) + } +} + +extension Vector: Codable { + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + let dx = try container.decode(Double.self) + let dy = try container.decode(Double.self) + self.init(dx: dx, dy: dy) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(dx) + try container.encode(dy) + } +} + +extension Vector { + @inlinable + public func applying(_ t: Transform) -> Vector { + Vector( + dx: t.a * dx + t.c * dy, + dy: t.b * dx + t.d * dy) + } +} diff --git a/Sources/CollectionsBenchmark/Charts/Graphics/Color.swift b/Sources/CollectionsBenchmark/Charts/Graphics/Color.swift index 2a4afcd..f49ef17 100644 --- a/Sources/CollectionsBenchmark/Charts/Graphics/Color.swift +++ b/Sources/CollectionsBenchmark/Charts/Graphics/Color.swift @@ -9,8 +9,6 @@ // //===----------------------------------------------------------------------===// -import Foundation // CGFloat - /// A color in sRGB color space, with an alpha channel and 8-bit components. public struct Color: Hashable { public var red: UInt8 @@ -25,21 +23,21 @@ public struct Color: Hashable { self.alpha = alpha } - public typealias CGFloatComponents = - (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) + public typealias RGBComponents = + (red: Double, green: Double, blue: Double, alpha: Double) - public init(srgbComponents srgb: CGFloatComponents) { + public init(srgbComponents srgb: RGBComponents) { self.red = UInt8((255 * min(max(srgb.red, 0), 1)).rounded()) self.green = UInt8((255 * min(max(srgb.green, 0), 1)).rounded()) self.blue = UInt8((255 * min(max(srgb.blue, 0), 1)).rounded()) self.alpha = UInt8((255 * min(max(srgb.alpha, 0), 1)).rounded()) } - public var srgbComponents: CGFloatComponents { - (CGFloat(red) / 255, - CGFloat(green) / 255, - CGFloat(blue) / 255, - CGFloat(alpha) / 255) + public var srgbComponents: RGBComponents { + (Double(red) / 255, + Double(green) / 255, + Double(blue) / 255, + Double(alpha) / 255) } } @@ -50,7 +48,7 @@ extension Color { } extension Color { - public func withAlphaFactor(_ value: CGFloat) -> Color { + public func withAlphaFactor(_ value: Double) -> Color { var comps = srgbComponents comps.alpha *= value return Self(srgbComponents: comps) @@ -59,10 +57,10 @@ extension Color { extension Color { public init( - hue: CGFloat, - saturation: CGFloat, - brightness: CGFloat, - alpha: CGFloat + hue: Double, + saturation: Double, + brightness: Double, + alpha: Double ) { let hue = hue - hue.rounded(.down) let segment = (6 * hue).rounded(.down) @@ -70,7 +68,7 @@ extension Color { let p = brightness * (1 - saturation) let q = brightness * (1 - saturation * fraction) let t = brightness * (1 - saturation * (1 - fraction)) - let srgb: CGFloatComponents + let srgb: RGBComponents switch Int(segment) % 6 { case 0: srgb = (red: brightness, green: t, blue: p, alpha: alpha) case 1: srgb = (red: q, green: brightness, blue: p, alpha: alpha) diff --git a/Sources/CollectionsBenchmark/Charts/Graphics/Font.swift b/Sources/CollectionsBenchmark/Charts/Graphics/Font.swift index 6f5e655..2ac1887 100644 --- a/Sources/CollectionsBenchmark/Charts/Graphics/Font.swift +++ b/Sources/CollectionsBenchmark/Charts/Graphics/Font.swift @@ -13,7 +13,7 @@ import Foundation public struct Font: Hashable, Codable, CustomStringConvertible { public var family: String - public var size: CGFloat + public var size: Double public var isBold = false public var isItalic = false diff --git a/Sources/CollectionsBenchmark/Charts/Graphics/Graphics+Element.swift b/Sources/CollectionsBenchmark/Charts/Graphics/Graphics+Element.swift index 0aaa767..2ae0f6b 100644 --- a/Sources/CollectionsBenchmark/Charts/Graphics/Graphics+Element.swift +++ b/Sources/CollectionsBenchmark/Charts/Graphics/Graphics+Element.swift @@ -9,13 +9,11 @@ // //===----------------------------------------------------------------------===// -import Foundation // For CGPoint, CGRect - extension Graphics { public enum Element { case shape(Shape) case text(Text) - case group(clippingRect: CGRect, [Element]) + case group(clippingRect: Rectangle, [Element]) } } @@ -30,7 +28,7 @@ extension Graphics.Element: Codable { self = .text(try container.decode(Text.self)) case "group": self = .group( - clippingRect: try container.decode(CGRect.self), + clippingRect: try container.decode(Rectangle.self), try container.decode([Self].self)) default: throw DecodingError.dataCorruptedError( diff --git a/Sources/CollectionsBenchmark/Charts/Graphics/Graphics.swift b/Sources/CollectionsBenchmark/Charts/Graphics/Graphics.swift index 322a9f1..7872160 100644 --- a/Sources/CollectionsBenchmark/Charts/Graphics/Graphics.swift +++ b/Sources/CollectionsBenchmark/Charts/Graphics/Graphics.swift @@ -9,14 +9,14 @@ // //===----------------------------------------------------------------------===// -import Foundation // For CGPoint, CGRect +import Foundation // For URL /// Just enough graphics to render basic 2D charts. public struct Graphics: Codable { - public var bounds: CGRect + public var bounds: Rectangle public var elements: [Element] = [] - public init(bounds: CGRect, elements: [Element] = []) { + public init(bounds: Rectangle, elements: [Element] = []) { self.bounds = bounds self.elements = elements } @@ -31,8 +31,8 @@ extension Graphics { } public mutating func addLine( - from start: CGPoint, - to end: CGPoint, + from start: Point, + to end: Point, stroke: Stroke? = nil ) { let path: Path = .line(from: start, to: end) @@ -40,7 +40,7 @@ extension Graphics { } public mutating func addRect( - _ rect: CGRect, + _ rect: Rectangle, fill: Color? = nil, stroke: Stroke? = nil ) { @@ -49,7 +49,7 @@ extension Graphics { } public mutating func addLines( - _ points: [CGPoint], + _ points: [Point], fill: Color? = nil, stroke: Stroke? = nil ) { @@ -61,8 +61,8 @@ extension Graphics { _ string: String, style: Text.Style, linkTarget: URL? = nil, - in boundingBox: CGRect, - descender: CGFloat + in boundingBox: Rectangle, + descender: Double ) { let text = Text( string, @@ -74,7 +74,7 @@ extension Graphics { } public mutating func addGroup( - clippingRect: CGRect, + clippingRect: Rectangle, contents: (inout Graphics) -> Void ) { var group = Graphics(bounds: clippingRect) diff --git a/Sources/CollectionsBenchmark/Charts/Graphics/Renderer.swift b/Sources/CollectionsBenchmark/Charts/Graphics/Renderer.swift index fe269f0..8a46af0 100644 --- a/Sources/CollectionsBenchmark/Charts/Graphics/Renderer.swift +++ b/Sources/CollectionsBenchmark/Charts/Graphics/Renderer.swift @@ -24,7 +24,7 @@ public protocol Renderer { func measure( _ font: Font, _ text: String - ) -> (size: CGSize, descender: CGFloat) + ) -> (width: Double, height: Double, descender: Double) var supportedImageFormats: [ImageFormat] { get } var defaultImageFormat: ImageFormat { get } @@ -32,7 +32,7 @@ public protocol Renderer { func render( _ graphics: Graphics, format: String, - bitmapScale: CGFloat + bitmapScale: Double ) throws -> Data func documentRenderer( diff --git a/Sources/CollectionsBenchmark/Charts/Graphics/Stroke.swift b/Sources/CollectionsBenchmark/Charts/Graphics/Stroke.swift index 8dc3007..9994af8 100644 --- a/Sources/CollectionsBenchmark/Charts/Graphics/Stroke.swift +++ b/Sources/CollectionsBenchmark/Charts/Graphics/Stroke.swift @@ -23,18 +23,18 @@ public struct Stroke: Hashable, Codable { case bevel } public struct Dash: Hashable, Codable { - public var style: [CGFloat] - public var phase: CGFloat = 0 + public var style: [Double] + public var phase: Double = 0 } - public var width: CGFloat + public var width: Double public var color: Color public var dash: Dash? public var capStyle: CapStyle public var joinStyle: JoinStyle public init( - width: CGFloat, + width: Double, color: Color, dash: Dash? = nil, capStyle: CapStyle = .round, diff --git a/Sources/CollectionsBenchmark/Charts/Graphics/Text.swift b/Sources/CollectionsBenchmark/Charts/Graphics/Text.swift index cb4f3fb..4a2a690 100644 --- a/Sources/CollectionsBenchmark/Charts/Graphics/Text.swift +++ b/Sources/CollectionsBenchmark/Charts/Graphics/Text.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -import Foundation // CGRect, URL +import Foundation // URL public struct Text: Codable { public struct Style: Hashable, Codable { @@ -19,16 +19,16 @@ public struct Text: Codable { public var string: String public var style: Style - public var boundingBox: CGRect - public var descender: CGFloat + public var boundingBox: Rectangle + public var descender: Double public var linkTarget: URL? public init( _ string: String, style: Style, linkTarget: URL? = nil, - in boundingBox: CGRect, - descender: CGFloat + in boundingBox: Rectangle, + descender: Double ) { self.string = string self.style = style diff --git a/Sources/CollectionsBenchmark/Charts/Renderers/CocoaRenderer.swift b/Sources/CollectionsBenchmark/Charts/Renderers/CocoaRenderer.swift index 064d724..4a667c9 100644 --- a/Sources/CollectionsBenchmark/Charts/Renderers/CocoaRenderer.swift +++ b/Sources/CollectionsBenchmark/Charts/Renderers/CocoaRenderer.swift @@ -49,27 +49,27 @@ extension NSBezierPath { switch path { case let .line(from: start, to: end): self.init() - move(to: start) - line(to: end) + move(to: CGPoint(start)) + line(to: CGPoint(end)) case let .rect(rect): - self.init(rect: rect) + self.init(rect: CGRect(rect)) case let .lines(points): self.init() if points.isEmpty { return } - self.move(to: points[0]) + self.move(to: CGPoint(points[0])) for point in points.dropFirst() { - self.line(to: point) + self.line(to: CGPoint(point)) } } } } extension NSImage { - internal func _pngData(scale: CGFloat = 4) throws -> Data { + internal func _pngData(scale: Double = 4) throws -> Data { let cgimage = self.cgImage( forProposedRect: nil, context: nil, - hints: [.ctm: NSAffineTransform(transform: .init(scale: scale))])! + hints: [.ctm: NSAffineTransform(transform: .init(scale: CGFloat(scale)))])! let rep = NSBitmapImageRep(cgImage: cgimage) rep.size = self.size guard let data = rep.representation(using: .png, properties: [:]) else { @@ -88,7 +88,7 @@ internal class _CocoaFontCache { let traits = font.fontDescriptor.symbolicTraits return Font( family: font.familyName ?? font.fontName, - size: font.pointSize, + size: Double(font.pointSize), isBold: traits.contains(.bold), isItalic: traits.contains(.italic)) } @@ -103,13 +103,13 @@ internal class _CocoaFontCache { let descriptor = NSFontDescriptor() .withFamily(font.family) .withSymbolicTraits(traits) - if let nsfont = NSFont(descriptor: descriptor, size: font.size) { + if let nsfont = NSFont(descriptor: descriptor, size: CGFloat(font.size)) { return nsfont } if _knownMissingFonts.insert(font).inserted { complain("warning: Missing font '\(font)' substituted with default") } - return NSFont.systemFont(ofSize: font.size) + return NSFont.systemFont(ofSize: CGFloat(font.size)) } } } @@ -122,16 +122,16 @@ public class CocoaRenderer: Renderer { public func measure( _ font: Font, _ text: String - ) -> (size: CGSize, descender: CGFloat) { + ) -> (width: Double, height: Double, descender: Double) { let font = _fontCache.nsFont(for: font) let size = (text as NSString).boundingRect( with: CGSize(width: 1000, height: 1000), options: [.usesFontLeading], attributes: [.font: font]).integral.size - return (size, -font.descender) + return (Double(size.width), Double(size.height), -Double(font.descender)) } - public func renderPNG(for graphics: Graphics, scale: CGFloat) throws -> Data { + public func renderPNG(for graphics: Graphics, scale: Double) throws -> Data { try renderImage(for: graphics)._pngData(scale: scale) } @@ -140,7 +140,7 @@ public class CocoaRenderer: Renderer { kCGPDFContextCreator: _projectName, ] let data = NSMutableData() - var bounds = graphics.bounds + var bounds = CGRect(graphics.bounds) let c = CGContext( consumer: CGDataConsumer(data: data as CFMutableData)!, mediaBox: &bounds, @@ -163,8 +163,10 @@ public class CocoaRenderer: Renderer { public func renderImage(for graphics: Graphics) -> NSImage { let b = graphics.bounds - return NSImage(size: b.integral.size, flipped: true) { rect in - let t = AffineTransform(translationByX: -b.minX, byY: -b.minY) + return NSImage(size: CGSize(b.integral.size), flipped: true) { rect in + let t = AffineTransform( + translationByX: CGFloat(-b.minX), + byY: CGFloat(-b.minY)) NSAffineTransform(transform: t).concat() self.draw(graphics) return true @@ -196,12 +198,14 @@ public class CocoaRenderer: Renderer { path.fill() } if let stroke = shape.stroke { - path.lineWidth = stroke.width + path.lineWidth = CGFloat(stroke.width) path.lineCapStyle = .init(stroke.capStyle) path.lineJoinStyle = .init(stroke.joinStyle) if let dash = stroke.dash { path.setLineDash( - dash.style, count: dash.style.count, phase: dash.phase) + dash.style.map { CGFloat($0) }, + count: dash.style.count, + phase: CGFloat(dash.phase)) } stroke.color.nsColor.setStroke() path.stroke() @@ -210,18 +214,18 @@ public class CocoaRenderer: Renderer { if let url = text.linkTarget { let c = NSGraphicsContext.current!.cgContext let tr = c.userSpaceToDeviceSpaceTransform - c.setURL(url as CFURL, for: text.boundingBox.applying(tr)) + c.setURL(url as CFURL, for: CGRect(text.boundingBox).applying(tr)) } var attributes: [NSAttributedString.Key: Any] = [:] attributes[.font] = _fontCache.nsFont(for: text.style.font) attributes[.foregroundColor] = text.style.color.nsColor //attributes[.link] = text.linkTarget let str = NSAttributedString(string: text.string, attributes: attributes) - str.draw(in: text.boundingBox) + str.draw(in: CGRect(text.boundingBox)) case let .group(clippingRect: clippingRect, elements): NSGraphicsContext.saveGraphicsState() - clippingRect.clip() + CGRect(clippingRect).clip() draw(elements) NSGraphicsContext.restoreGraphicsState() } @@ -236,7 +240,7 @@ public class CocoaRenderer: Renderer { public func render( _ graphics: Graphics, format: String, - bitmapScale: CGFloat + bitmapScale: Double ) throws -> Data { switch format.lowercased() { case "png": @@ -244,7 +248,8 @@ public class CocoaRenderer: Renderer { case "pdf": return try renderPDF(for: graphics) default: - return try DefaultRenderer().render(graphics, format: format, bitmapScale: bitmapScale) + return try DefaultRenderer() + .render(graphics, format: format, bitmapScale: bitmapScale) } } @@ -255,7 +260,8 @@ public class CocoaRenderer: Renderer { ) throws -> DocumentRenderer { switch format { default: - return try DefaultRenderer().documentRenderer(title: title, format: format, style: style) + return try DefaultRenderer() + .documentRenderer(title: title, format: format, style: style) } } } diff --git a/Sources/CollectionsBenchmark/Charts/Renderers/CoreGraphics.swift b/Sources/CollectionsBenchmark/Charts/Renderers/CoreGraphics.swift index 9a3ef0a..8fcb6fe 100644 --- a/Sources/CollectionsBenchmark/Charts/Renderers/CoreGraphics.swift +++ b/Sources/CollectionsBenchmark/Charts/Renderers/CoreGraphics.swift @@ -12,6 +12,75 @@ #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) import CoreGraphics +extension CGPoint { + public init(_ point: Point) { + self.init(x: point.x, y: point.y) + } +} + +extension Point { + public init(_ point: CGPoint) { + self.init(x: Double(point.x), y: Double(point.y)) + } +} + +extension CGSize { + public init(_ vector: Vector) { + self.init(width: CGFloat(vector.dx), height: CGFloat(vector.dy)) + } +} + +extension Vector { + public init(_ size: CGSize) { + self.init(dx: Double(size.width), dy: Double(size.height)) + } +} + +extension CGRect { + public init(_ rect: Rectangle) { + self.init( + x: rect.origin.x, + y: rect.origin.y, + width: rect.size.dx, + height: rect.size.dy) + } +} + +extension Rectangle { + public init(_ rect: CGRect) { + self.init( + x: Double(rect.origin.x), + y: Double(rect.origin.y), + width: Double(rect.size.width), + height: Double(rect.size.height)) + } +} + +extension CGAffineTransform { + public init(_ t: Transform) { + self.init( + a: CGFloat(t.a), + b: CGFloat(t.b), + c: CGFloat(t.c), + d: CGFloat(t.d), + tx: CGFloat(t.tx), + ty: CGFloat(t.ty)) + } +} + +extension Transform { + public init(_ t: CGAffineTransform) { + self.init( + a: Double(t.a), + b: Double(t.b), + c: Double(t.c), + d: Double(t.d), + tx: Double(t.tx), + ty: Double(t.ty)) + } +} + + extension Color { @available(macOS 10.11, *) public init?(_ color: CGColor) { @@ -23,21 +92,30 @@ extension Color { else { return nil } - self.init(srgbComponents: (comps[0], comps[1], comps[2], comps[3])) + self.init( + srgbComponents: ( + Double(comps[0]), + Double(comps[1]), + Double(comps[2]), + Double(comps[3]))) } public var cgColor: CGColor { let comps = srgbComponents if #available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) { return CGColor( - srgbRed: comps.red, - green: comps.green, - blue: comps.blue, - alpha: comps.alpha) + srgbRed: CGFloat(comps.red), + green: CGFloat(comps.green), + blue: CGFloat(comps.blue), + alpha: CGFloat(comps.alpha)) } else { return CGColor( colorSpace: CGColorSpace(name: CGColorSpace.sRGB)!, - components: [comps.red, comps.green, comps.blue, comps.alpha])! + components: [ + CGFloat(comps.red), + CGFloat(comps.green), + CGFloat(comps.blue), + CGFloat(comps.alpha)])! } } } diff --git a/Sources/CollectionsBenchmark/Charts/Renderers/DefaultRenderer.swift b/Sources/CollectionsBenchmark/Charts/Renderers/DefaultRenderer.swift index ec5d863..242083b 100644 --- a/Sources/CollectionsBenchmark/Charts/Renderers/DefaultRenderer.swift +++ b/Sources/CollectionsBenchmark/Charts/Renderers/DefaultRenderer.swift @@ -39,18 +39,17 @@ public struct DefaultRenderer: Renderer { public func measure( _ font: Font, _ text: String - ) -> (size: CGSize, descender: CGFloat) { - let unitAdvancement = CGSize(width: 0.60009765625, height: 1.1328125) - let unitDescender: CGFloat = 0.30029296875 + ) -> (width: Double, height: Double, descender: Double) { + let unitAdvancement = Vector(dx: 0.60009765625, dy: 1.1328125) + let unitDescender = 0.30029296875 - let width = CGFloat(text.count) * font.size * unitAdvancement.width - let height = font.size * unitAdvancement.height + let width = Double(text.count) * font.size * unitAdvancement.dx + let height = font.size * unitAdvancement.dy let descender = unitDescender * font.size return ( - size: CGSize( - width: width.rounded(.up), - height: height.rounded(.up)), + width: width.rounded(.up), + height: height.rounded(.up), descender: descender) } @@ -60,7 +59,7 @@ public struct DefaultRenderer: Renderer { public func render( _ graphics: Graphics, format: String, - bitmapScale: CGFloat + bitmapScale: Double ) throws -> Data { switch format.lowercased() { case "json": diff --git a/Sources/CollectionsBenchmark/Charts/Renderers/UIKitRenderer.swift b/Sources/CollectionsBenchmark/Charts/Renderers/UIKitRenderer.swift index 8c2a0f3..a18aa9b 100644 --- a/Sources/CollectionsBenchmark/Charts/Renderers/UIKitRenderer.swift +++ b/Sources/CollectionsBenchmark/Charts/Renderers/UIKitRenderer.swift @@ -27,16 +27,16 @@ extension UIBezierPath { switch path { case let .line(from: start, to: end): self.init() - move(to: start) - addLine(to: end) + move(to: CGPoint(start)) + addLine(to: CGPoint(end)) case let .rect(rect): - self.init(rect: rect) + self.init(rect: CGRect(rect)) case let .lines(points): self.init() if points.isEmpty { return } - self.move(to: points[0]) + self.move(to: CGPoint(points[0])) for point in points.dropFirst() { - self.addLine(to: point) + self.addLine(to: CGPoint(point)) } } } @@ -57,7 +57,7 @@ internal class _UIKitFontCache { let descriptor = UIFontDescriptor() .withFamily(font.family) .withSymbolicTraits(traits)! - return UIFont(descriptor: descriptor, size: font.size) + return UIFont(descriptor: descriptor, size: CGFloat(font.size)) } } @@ -65,7 +65,7 @@ internal class _UIKitFontCache { let traits = font.fontDescriptor.symbolicTraits return Font( family: font.familyName, - size: font.pointSize, + size: Double(font.pointSize), isBold: traits.contains(.traitBold), isItalic: traits.contains(.traitItalic)) } @@ -79,7 +79,7 @@ public struct UIKitRenderer: Renderer { public func measure( _ font: Font, _ text: String - ) -> (size: CGSize, descender: CGFloat) { + ) -> (width: Double, height: Double, descender: Double) { let font = _fontCache.uiFont(for: font) let size = (text as NSString).boundingRect( with: CGSize(width: 1000, height: 1000), @@ -87,12 +87,12 @@ public struct UIKitRenderer: Renderer { attributes: [.font: font], context: nil ).integral.size - return (size, -font.descender) + return (Double(size.width), Double(size.height), -Double(font.descender)) } #if !os(watchOS) @available(iOS 10, tvOS 10, *) - public func renderPNG(for graphics: Graphics, scale: CGFloat) throws -> Data { + public func renderPNG(for graphics: Graphics, scale: Double) throws -> Data { guard let data = renderBitmap(for: graphics, scale: scale).pngData() else { throw Benchmark.Error("Error generating PNG data") } @@ -101,16 +101,16 @@ public struct UIKitRenderer: Renderer { @available(iOS 10, tvOS 10, *) public func renderPDF(for graphics: Graphics) throws -> Data { - let renderer = UIGraphicsPDFRenderer(bounds: graphics.bounds) + let renderer = UIGraphicsPDFRenderer(bounds: CGRect(graphics.bounds)) return renderer.pdfData { context in - context.beginPage(withBounds: graphics.bounds, + context.beginPage(withBounds: CGRect(graphics.bounds), pageInfo: [kCGPDFContextCreator as String: _projectName]) draw(graphics.elements) } } @available(iOS 10, tvOS 10, *) - public func renderBitmap(for graphics: Graphics, scale: CGFloat) -> UIImage { + public func renderBitmap(for graphics: Graphics, scale: Double) -> UIImage { let srgbTraits = UITraitCollection(displayGamut: .SRGB) let format: UIGraphicsImageRendererFormat if #available(iOS 11, tvOS 11, *) { @@ -122,9 +122,9 @@ public struct UIKitRenderer: Renderer { if #available(iOS 12, tvOS 12, *) { format.preferredRange = .standard } - format.scale = scale + format.scale = CGFloat(scale) let renderer = UIGraphicsImageRenderer( - bounds: graphics.bounds, + bounds: CGRect(graphics.bounds), format: format) return renderer.image { _ in draw(graphics.elements) @@ -158,12 +158,14 @@ public struct UIKitRenderer: Renderer { } if let stroke = shape.stroke { - path.lineWidth = stroke.width + path.lineWidth = CGFloat(stroke.width) path.lineCapStyle = .init(stroke.capStyle) path.lineJoinStyle = .init(stroke.joinStyle) if let dash = stroke.dash { path.setLineDash( - dash.style, count: dash.style.count, phase: dash.phase) + dash.style.map { CGFloat($0) }, + count: dash.style.count, + phase: CGFloat(dash.phase)) } stroke.color.uiColor.setStroke() path.stroke() @@ -172,17 +174,19 @@ public struct UIKitRenderer: Renderer { if let url = text.linkTarget { let c = UIGraphicsGetCurrentContext()! let tr = c.userSpaceToDeviceSpaceTransform - UIGraphicsSetPDFContextURLForRect(url, text.boundingBox.applying(tr)) + UIGraphicsSetPDFContextURLForRect( + url, + CGRect(text.boundingBox).applying(tr)) } var attributes: [NSAttributedString.Key: Any] = [:] attributes[.font] = _fontCache.uiFont(for: text.style.font) attributes[.foregroundColor] = text.style.color.uiColor let str = NSAttributedString(string: text.string, attributes: attributes) - str.draw(in: text.boundingBox) + str.draw(in: CGRect(text.boundingBox)) case let .group(clippingRect: clippingRect, elements): let c = UIGraphicsGetCurrentContext()! c.saveGState() - c.clip(to: clippingRect) + c.clip(to: CGRect(clippingRect)) draw(elements) c.restoreGState() } @@ -203,7 +207,7 @@ public struct UIKitRenderer: Renderer { public func render( _ graphics: Graphics, format: String, - bitmapScale: CGFloat + bitmapScale: Double ) throws -> Data { let format = format.lowercased() #if !os(watchOS) diff --git a/Sources/CollectionsBenchmark/Charts/Theme.swift b/Sources/CollectionsBenchmark/Charts/Theme.swift index 43b7959..62e7c08 100644 --- a/Sources/CollectionsBenchmark/Charts/Theme.swift +++ b/Sources/CollectionsBenchmark/Charts/Theme.swift @@ -39,26 +39,26 @@ public struct Theme: Codable { public var axisLabels = Text.Style( font: Font(family: "Helvetica", size: 10), color: .black) - public var axisLeading: CGFloat = 3 + public var axisLeading: Double = 3 public var curves: [CurveTheme] = [] public var curveFallback: CurveTheme = CurveTheme(color: .black, width: 4) public var hairlines = Stroke(width: 0.5, color: .black) - public var bandDimmingFactor: CGFloat = 0.3 + public var bandDimmingFactor: Double = 0.3 - public var xPadding: CGFloat = 6 - public var yPadding: CGFloat = 3 + public var xPadding: Double = 6 + public var yPadding: Double = 3 public var legendPosition: LegendPosition = .topLeft public var legendLabels = Text.Style(font: Font(family: "Menlo", size: 12), color: .black) - public var legendCornerOffset = CGPoint(x: 24, y: 24) + public var legendCornerOffset = Point(x: 24, y: 24) public var legendPadding = EdgeInsets(top: 12, left: 12, bottom: 12, right: 12) - public var legendLineSampleWidth: CGFloat = 24 - public var legendLineLeading: CGFloat = 3 - public var legendSeparation: CGFloat = 9 + public var legendLineSampleWidth: Double = 24 + public var legendLineLeading: Double = 3 + public var legendSeparation: Double = 9 public init() {} } @@ -66,9 +66,9 @@ public struct Theme: Codable { extension Theme { public struct CurveTheme: Hashable, Codable { public var color: Color - public var lineWidth: CGFloat + public var lineWidth: Double - public init(color: Color, width: CGFloat) { + public init(color: Color, width: Double) { self.color = color self.lineWidth = width } @@ -89,7 +89,7 @@ extension Theme { // When we have to draw too many curves, just spread them out equally // over the rainbow. The result typically won't be very useful. theme.color = Color( - hue: CGFloat(index) / CGFloat(count), + hue: Double(index) / Double(count), saturation: 1, brightness: 1, alpha: 1) @@ -102,25 +102,29 @@ extension Theme { .withAlphaFactor(bandDimmingFactor) } - internal func _legendFrame(for size: CGSize, in bounds: CGRect) -> CGRect { - let origin: CGPoint + internal func _legendFrame(for size: Vector, in bounds: Rectangle) -> Rectangle { + let origin: Point switch legendPosition { case .hidden: - return CGRect.null + return Rectangle.null case .topLeft: - origin = CGPoint(x: bounds.minX + legendCornerOffset.x, - y: bounds.minY + legendCornerOffset.y) + origin = Point( + x: bounds.minX + legendCornerOffset.x, + y: bounds.minY + legendCornerOffset.y) case .topRight: - origin = CGPoint(x: bounds.maxX - legendCornerOffset.x - size.width, - y: bounds.minY + legendCornerOffset.y) + origin = Point( + x: bounds.maxX - legendCornerOffset.x - size.dx, + y: bounds.minY + legendCornerOffset.y) case .bottomLeft: - origin = CGPoint(x: bounds.minX + legendCornerOffset.x, - y: bounds.maxY - legendCornerOffset.y - size.height) + origin = Point( + x: bounds.minX + legendCornerOffset.x, + y: bounds.maxY - legendCornerOffset.y - size.dy) case .bottomRight: - origin = CGPoint(x: bounds.maxX - legendCornerOffset.x - size.width, - y: bounds.maxY - legendCornerOffset.y - size.height) + origin = Point( + x: bounds.maxX - legendCornerOffset.x - size.dx, + y: bounds.maxY - legendCornerOffset.y - size.dy) } - return CGRect(origin: origin, size: size) + return Rectangle(origin: origin, size: size) } } diff --git a/Sources/CollectionsBenchmark/Compatibility/Compatibility.swift b/Sources/CollectionsBenchmark/Compatibility/Compatibility.swift new file mode 100644 index 0000000..4c70900 --- /dev/null +++ b/Sources/CollectionsBenchmark/Compatibility/Compatibility.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension BenchmarkResults { + // Deprecated since 0.0.2 + @available(*, deprecated, renamed: "Measurement") + public typealias Point = Measurement +} diff --git a/Sources/CollectionsBenchmark/Utilities/CGTypes.swift b/Sources/CollectionsBenchmark/Utilities/CGTypes.swift deleted file mode 100644 index 273ba3e..0000000 --- a/Sources/CollectionsBenchmark/Utilities/CGTypes.swift +++ /dev/null @@ -1,29 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift Collections open source project -// -// Copyright (c) 2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// -//===----------------------------------------------------------------------===// - -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) -import CoreGraphics -import Foundation - -public typealias CGFloat = CoreGraphics.CGFloat -public typealias CGPoint = CoreGraphics.CGPoint -public typealias CGSize = CoreGraphics.CGSize -public typealias CGRect = CoreGraphics.CGRect -public typealias AffineTransform = Foundation.AffineTransform -#else -import Foundation - -public typealias CGFloat = Foundation.CGFloat -public typealias CGPoint = Foundation.CGPoint -public typealias CGSize = Foundation.CGSize -public typealias CGRect = Foundation.CGRect -public typealias AffineTransform = Foundation.AffineTransform -#endif diff --git a/Sources/CollectionsBenchmark/Utilities/_Shims.swift b/Sources/CollectionsBenchmark/Utilities/_Shims.swift new file mode 100644 index 0000000..2f4cc71 --- /dev/null +++ b/Sources/CollectionsBenchmark/Utilities/_Shims.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import Foundation + +@inline(__always) +internal func _sin(_ radians: Double) -> Double { + sin(radians) +} + +@inline(__always) +internal func _cos(_ radians: Double) -> Double { + cos(radians) +} + +@inline(__always) +internal func _log2(_ value: Double) -> Double { + log2(value) +} + +@inline(__always) +internal func _pow(_ base: Double, _ exponent: Double) -> Double { + pow(base, exponent) +} diff --git a/Tests/CollectionsBenchmarkTests/GeometryTests.swift b/Tests/CollectionsBenchmarkTests/GeometryTests.swift new file mode 100644 index 0000000..6411229 --- /dev/null +++ b/Tests/CollectionsBenchmarkTests/GeometryTests.swift @@ -0,0 +1,428 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import CollectionsBenchmark + +final class GeometryTests: XCTestCase { + func testPoint() { + let p1 = Point(x: 1, y: 2) + XCTAssertEqual(p1.x, 1) + XCTAssertEqual(p1.y, 2) + + let p2 = Point(x: 2, y: 1) + XCTAssertEqual(p2.x, 2) + XCTAssertEqual(p2.y, 1) + + XCTAssertEqual(p1, Point(x: 1, y: 2)) + XCTAssertNotEqual(p1, p2) + + var hasher = Hasher() + hasher.combine(1 as Double) + hasher.combine(2 as Double) + XCTAssertEqual(p1.hashValue, hasher.finalize()) + + XCTAssertEqual(Point.zero.x, 0) + XCTAssertEqual(Point.zero.y, 0) + } + + func testVector() { + let v1 = Vector(dx: 1, dy: 2) + XCTAssertEqual(v1.dx, 1) + XCTAssertEqual(v1.dy, 2) + + let v2 = Vector(dx: 2, dy: 1) + XCTAssertEqual(v2.dx, 2) + XCTAssertEqual(v2.dy, 1) + + XCTAssertEqual(v1, Vector(dx: 1, dy: 2)) + XCTAssertNotEqual(v1, v2) + + var hasher = Hasher() + hasher.combine(1 as Double) + hasher.combine(2 as Double) + XCTAssertEqual(v1.hashValue, hasher.finalize()) + + XCTAssertEqual(Vector.zero.dx, 0) + XCTAssertEqual(Vector.zero.dy, 0) + } + + func testRectangle() { + let r1 = Rectangle(x: 1, y: 2, width: 3, height: 4) + XCTAssertEqual(r1.origin, Point(x: 1, y: 2)) + XCTAssertEqual(r1.size, Vector(dx: 3, dy: 4)) + XCTAssertEqual(r1.minX, 1) + XCTAssertEqual(r1.maxX, 4) + XCTAssertEqual(r1.midX, 2.5) + XCTAssertEqual(r1.minY, 2) + XCTAssertEqual(r1.maxY, 6) + XCTAssertEqual(r1.midY, 4) + + let r2 = Rectangle(origin: Point(x: 4, y: 6), size: Vector(dx: -3, dy: -4)) + XCTAssertEqual(r2.origin, Point(x: 4, y: 6)) + XCTAssertEqual(r2.size, Vector(dx: -3, dy: -4)) + XCTAssertEqual(r2.minX, 1) + XCTAssertEqual(r2.maxX, 4) + XCTAssertEqual(r2.midX, 2.5) + XCTAssertEqual(r2.minY, 2) + XCTAssertEqual(r2.maxY, 6) + XCTAssertEqual(r2.midY, 4) + + let r3 = Rectangle( + x: 4 as Float, y: 3 as Float, width: 2 as Float, height: 1 as Float) + + XCTAssertEqual(r1, r1) + XCTAssertEqual(r1, r2) + XCTAssertNotEqual(r1, r3) + + XCTAssertEqual(r2, r1) + XCTAssertEqual(r2, r2) + XCTAssertNotEqual(r2, r3) + + XCTAssertNotEqual(r3, r1) + XCTAssertNotEqual(r3, r2) + XCTAssertEqual(r3, r3) + + var hasher = Hasher() + hasher.combine(1 as Double) + hasher.combine(2 as Double) + hasher.combine(3 as Double) + hasher.combine(4 as Double) + let hash = hasher.finalize() + XCTAssertEqual(r1.hashValue, hash) + XCTAssertEqual(r2.hashValue, hash) + + XCTAssertEqual(Rectangle.null.origin.x, .infinity) + XCTAssertEqual(Rectangle.null.origin.y, .infinity) + XCTAssertEqual(Rectangle.null.size.dx, 0) + XCTAssertEqual(Rectangle.null.size.dy, 0) + } + + func testTransform() throws { + let t1 = Transform(a: 1, b: 2, c: 3, d: 4, tx: 5, ty: 6) + XCTAssertEqual(t1.a, 1) + XCTAssertEqual(t1.b, 2) + XCTAssertEqual(t1.c, 3) + XCTAssertEqual(t1.d, 4) + XCTAssertEqual(t1.tx, 5) + XCTAssertEqual(t1.ty, 6) + + let t2 = Transform(a: -1, b: -2, c: -3, d: -4, tx: -5, ty: -6) + XCTAssertEqual(t2.a, -1) + XCTAssertEqual(t2.b, -2) + XCTAssertEqual(t2.c, -3) + XCTAssertEqual(t2.d, -4) + XCTAssertEqual(t2.tx, -5) + XCTAssertEqual(t2.ty, -6) + + XCTAssertEqual(t1, t1) + XCTAssertNotEqual(t1, t2) + XCTAssertEqual(t2, t2) + + var hasher = Hasher() + hasher.combine(1 as Double) + hasher.combine(2 as Double) + hasher.combine(3 as Double) + hasher.combine(4 as Double) + hasher.combine(5 as Double) + hasher.combine(6 as Double) + let hash = hasher.finalize() + XCTAssertEqual(t1.hashValue, hash) + } + + func testPoint_Codable() throws { + let p = Point(x: 1, y: 2) + let encoder = JSONEncoder() + let data = try encoder.encode(p) + let decoder = JSONDecoder() + let q = try decoder.decode(Point.self, from: data) + XCTAssertEqual(q, p) + } + + func testVector_Codable() throws { + let v = Vector(dx: 1, dy: 2) + let encoder = JSONEncoder() + let data = try encoder.encode(v) + let decoder = JSONDecoder() + let w = try decoder.decode(Vector.self, from: data) + XCTAssertEqual(w, v) + } + + func testRectangle_Codable() throws { + let r1 = Rectangle(x: 1, y: 2, width: 3, height: 4) + let encoder = JSONEncoder() + let data = try encoder.encode(r1) + let decoder = JSONDecoder() + let r2 = try decoder.decode(Rectangle.self, from: data) + XCTAssertEqual(r1, r2) + } + + func testTransform_Codable() throws { + let t1 = Transform(a: 1, b: 2, c: 3, d: 4, tx: 5, ty: 6) + let encoder = JSONEncoder() + let data = try encoder.encode(t1) + let decoder = JSONDecoder() + let t2 = try decoder.decode(Transform.self, from: data) + XCTAssertEqual(t1, t2) + } + + func testRectangle_isNull() throws { + XCTAssertTrue(Rectangle.null.isNull) + XCTAssertFalse(Rectangle(x: 1, y: 2, width: 3, height: 4).isNull) + XCTAssertTrue(Rectangle(x: .infinity, y: 2, width: 3, height: 4).isNull) + XCTAssertFalse(Rectangle(x: -.infinity, y: 2, width: 3, height: 4).isNull) + XCTAssertTrue(Rectangle(x: 1, y: .infinity, width: 3, height: 4).isNull) + XCTAssertFalse(Rectangle(x: 1, y: -.infinity, width: 3, height: 4).isNull) + XCTAssertFalse(Rectangle(x: 1, y: 2, width: .infinity, height: 4).isNull) + XCTAssertFalse(Rectangle(x: 1, y: 2, width: -.infinity, height: 4).isNull) + XCTAssertFalse(Rectangle(x: 1, y: 2, width: 3, height: .infinity).isNull) + XCTAssertFalse(Rectangle(x: 1, y: 2, width: 3, height: -.infinity).isNull) + } + + func testRectangle_intersects() throws { + let r0 = Rectangle.null + let r1 = Rectangle(x: 0, y: 0, width: 1, height: 2) + let r2 = Rectangle(x: 1, y: 0, width: 1, height: 2) + let r3 = Rectangle(x: 0, y: 2, width: 1, height: 2) + let r4 = Rectangle(x: 0, y: 0, width: 2, height: 4) + + XCTAssertFalse(r1.intersects(r0)) + XCTAssertTrue(r1.intersects(r1)) + XCTAssertFalse(r1.intersects(r2)) + XCTAssertFalse(r1.intersects(r3)) + XCTAssertTrue(r1.intersects(r4)) + + XCTAssertFalse(r2.intersects(r0)) + XCTAssertFalse(r2.intersects(r1)) + XCTAssertTrue(r2.intersects(r2)) + XCTAssertFalse(r2.intersects(r3)) + XCTAssertTrue(r2.intersects(r4)) + + XCTAssertFalse(r3.intersects(r0)) + XCTAssertFalse(r3.intersects(r1)) + XCTAssertFalse(r3.intersects(r2)) + XCTAssertTrue(r3.intersects(r3)) + XCTAssertTrue(r3.intersects(r4)) + + XCTAssertFalse(r4.intersects(r0)) + XCTAssertTrue(r4.intersects(r1)) + XCTAssertTrue(r4.intersects(r2)) + XCTAssertTrue(r4.intersects(r3)) + XCTAssertTrue(r4.intersects(r4)) + } + + func testRectangle_inset() throws { + let r = Rectangle(x: 1, y: 2, width: 3, height: 4) + + XCTAssertEqual( + r.inset(by: EdgeInsets(top: 0.125, left: 0.25, bottom: 0.375, right: 0.5)), + Rectangle(x: 1.25, y: 2.125, width: 2.25, height: 3.5)) + + XCTAssertEqual( + r.inset(by: EdgeInsets(top: 4, left: 3, bottom: 0, right: 0)), + Rectangle(x: 4, y: 6, width: 0, height: 0)) + + XCTAssertEqual( + r.inset(by: EdgeInsets(top: 0, left: 3.25, bottom: 4, right: 0)), + .null) + + XCTAssertEqual( + r.inset(by: EdgeInsets(top: 0, left: 3, bottom: 4.25, right: 0)), + .null) + + XCTAssertEqual( + r.inset(dx: 0.5, dy: 0.5), + Rectangle(x: 1.5, y: 2.5, width: 2, height: 3)) + } + + func testRectangle_standardized() throws { + XCTAssertEqual( + Rectangle(x: 1, y: 2, width: 3, height: 4).standardized, + Rectangle(x: 1, y: 2, width: 3, height: 4)) + XCTAssertEqual( + Rectangle(x: 1, y: 2, width: -3, height: 4).standardized, + Rectangle(x: -2, y: 2, width: 3, height: 4)) + XCTAssertEqual( + Rectangle(x: 1, y: 2, width: 3, height: -4).standardized, + Rectangle(x: 1, y: -2, width: 3, height: 4)) + XCTAssertEqual( + Rectangle(x: 1, y: 2, width: -3, height: -4).standardized, + Rectangle(x: -2, y: -2, width: 3, height: 4)) + } + + func testRectangle_integral() throws { + XCTAssertEqual( + Rectangle(x: 1, y: 2, width: 3, height: 4).integral, + Rectangle(x: 1, y: 2, width: 3, height: 4)) + XCTAssertEqual( + Rectangle(x: 1.5, y: 2.5, width: 3.5, height: 4.5).integral, + Rectangle(x: 1, y: 2, width: 4, height: 5)) + XCTAssertEqual( + Rectangle(x: -1.5, y: -2.5, width: 3.5, height: 4.5).integral, + Rectangle(x: -2, y: -3, width: 4, height: 5)) + XCTAssertEqual( + Rectangle(x: 1, y: 2, width: -3.5, height: -4.5).integral, + Rectangle(x: -3, y: -3, width: 4, height: 5)) + } + + func testRectangle_divided() throws { + XCTAssertEqual(Rectangle.null.divided(atDistance: 1, from: .minX).slice, .null) + XCTAssertEqual(Rectangle.null.divided(atDistance: 1, from: .minX).remainder, .null) + + XCTAssertEqual(Rectangle.null.divided(atDistance: 1, from: .maxX).slice, .null) + XCTAssertEqual(Rectangle.null.divided(atDistance: 1, from: .maxX).remainder, .null) + + XCTAssertEqual(Rectangle.null.divided(atDistance: 1, from: .minY).slice, .null) + XCTAssertEqual(Rectangle.null.divided(atDistance: 1, from: .minY).remainder, .null) + + XCTAssertEqual(Rectangle.null.divided(atDistance: 1, from: .maxY).slice, .null) + XCTAssertEqual(Rectangle.null.divided(atDistance: 1, from: .maxY).remainder, .null) + + let r = Rectangle(x: 1, y: 2, width: 3, height: 4) + + // minX + XCTAssertEqual( + r.divided(atDistance: 0.5, from: .minX).slice, + Rectangle(x: 1, y: 2, width: 0.5, height: 4)) + XCTAssertEqual( + r.divided(atDistance: 0.5, from: .minX).remainder, + Rectangle(x: 1.5, y: 2, width: 2.5, height: 4)) + + XCTAssertEqual( + r.divided(atDistance: -0.5, from: .minX).slice, + Rectangle(x: 1, y: 2, width: 0, height: 4)) + XCTAssertEqual( + r.divided(atDistance: -0.5, from: .minX).remainder, + Rectangle(x: 1, y: 2, width: 3, height: 4)) + + // maxX + XCTAssertEqual( + r.divided(atDistance: 0.5, from: .maxX).slice, + Rectangle(x: 3.5, y: 2, width: 0.5, height: 4)) + XCTAssertEqual( + r.divided(atDistance: 0.5, from: .maxX).remainder, + Rectangle(x: 1, y: 2, width: 2.5, height: 4)) + + XCTAssertEqual( + r.divided(atDistance: -0.5, from: .maxX).slice, + Rectangle(x: 4, y: 2, width: 0, height: 4)) + XCTAssertEqual( + r.divided(atDistance: -0.5, from: .maxX).remainder, + Rectangle(x: 1, y: 2, width: 3, height: 4)) + + // minY + XCTAssertEqual( + r.divided(atDistance: 0.5, from: .minY).slice, + Rectangle(x: 1, y: 2, width: 3, height: 0.5)) + XCTAssertEqual( + r.divided(atDistance: 0.5, from: .minY).remainder, + Rectangle(x: 1, y: 2.5, width: 3, height: 3.5)) + + XCTAssertEqual( + r.divided(atDistance: -0.5, from: .minY).slice, + Rectangle(x: 1, y: 2, width: 3, height: 0)) + XCTAssertEqual( + r.divided(atDistance: -0.5, from: .minY).remainder, + Rectangle(x: 1, y: 2, width: 3, height: 4)) + + // maxY + XCTAssertEqual( + r.divided(atDistance: 0.5, from: .maxY).slice, + Rectangle(x: 1, y: 5.5, width: 3, height: 0.5)) + XCTAssertEqual( + r.divided(atDistance: 0.5, from: .maxY).remainder, + Rectangle(x: 1, y: 2, width: 3, height: 3.5)) + + XCTAssertEqual( + r.divided(atDistance: -0.5, from: .maxY).slice, + Rectangle(x: 1, y: 6, width: 3, height: 0)) + XCTAssertEqual( + r.divided(atDistance: -0.5, from: .maxY).remainder, + Rectangle(x: 1, y: 2, width: 3, height: 4)) + } + + func testTransform_identity() { + XCTAssertEqual(Transform.identity, Transform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0)) + XCTAssertEqual(Transform.identity.concatenating(.identity), .identity) + XCTAssertEqual(Point(x: 1, y: 2).applying(.identity), Point(x: 1, y: 2)) + XCTAssertEqual(Vector(dx: 1, dy: 2).applying(.identity), Vector(dx: 1, dy: 2)) + } + + func testTransform_concatenating() { + let a = Transform(a: 1, b: 2, c: 3, d: 4, tx: 5, ty: 6) + let b = Transform(a: 0.5, b: 1.0, c: 1.5, d: 2, tx: 2.5, ty: 3) + let c = Transform(a: 6, b: 5, c: 4, d: 3, tx: 2, ty: 1) + + XCTAssertEqual( + a.concatenating(b), + Transform(a: 3.5, b: 5, c: 7.5, d: 11, tx: 14, ty: 20)) + XCTAssertEqual( + b.concatenating(a), + Transform(a: 3.5, b: 5, c: 7.5, d: 11, tx: 16.5, ty: 23)) + + XCTAssertEqual( + a.concatenating(c), + Transform(a: 14, b: 11, c: 34, d: 27, tx: 56, ty: 44)) + XCTAssertEqual( + c.concatenating(a), + Transform(a: 21, b: 32, c: 13, d: 20, tx: 10, ty: 14)) + + XCTAssertEqual( + b.concatenating(c), + Transform(a: 7, b: 5.5, c: 17, d: 13.5, tx: 29, ty: 22.5)) + XCTAssertEqual( + c.concatenating(b), + Transform(a: 10.5, b: 16, c: 6.5, d: 10, tx: 5, ty: 7)) + } + + func testTransform_scaled() { + let scale = Transform.identity.scaled(x: 2, y: 0.5) + XCTAssertEqual(scale, Transform(a: 2, b: 0, c: 0, d: 0.5, tx: 0, ty: 0)) + XCTAssertEqual(scale.scaled(2), Transform(a: 4, b: 0, c: 0, d: 1, tx: 0, ty: 0)) + XCTAssertEqual(Point(x: 1, y: 2).applying(scale), Point(x: 2, y: 1)) + XCTAssertEqual(Vector(dx: 1, dy: 2).applying(scale), Vector(dx: 2, dy: 1)) + } + + func testTransform_translated() { + let translation = Transform.identity.translated(x: 3, y: 4) + XCTAssertEqual(translation, Transform(a: 1, b: 0, c: 0, d: 1, tx: 3, ty: 4)) + XCTAssertEqual(translation.translated(x: 1, y: 2), + Transform(a: 1, b: 0, c: 0, d: 1, tx: 4, ty: 6)) + XCTAssertEqual(Point(x: 1, y: 2).applying(translation), Point(x: 4, y: 6)) + XCTAssertEqual(Vector(dx: 1, dy: 2).applying(translation), Vector(dx: 1, dy: 2)) + } + + func testTransform_rotated() { + let rotation = Transform.identity.rotated(Double.pi / 2) + XCTAssertEqual(rotation.a, 0, accuracy: 0.001) + XCTAssertEqual(rotation.b, 1, accuracy: 0.001) + XCTAssertEqual(rotation.c, -1, accuracy: 0.001) + XCTAssertEqual(rotation.d, 0, accuracy: 0.001) + XCTAssertEqual(rotation.tx, 0, accuracy: 0.001) + XCTAssertEqual(rotation.ty, 0, accuracy: 0.001) + + let r4 = rotation.rotated(.pi / 2).rotated(.pi / 2).rotated(.pi / 2) + XCTAssertEqual(r4.a, 1, accuracy: 0.001) + XCTAssertEqual(r4.b, 0, accuracy: 0.001) + XCTAssertEqual(r4.c, 0, accuracy: 0.001) + XCTAssertEqual(r4.d, 1, accuracy: 0.001) + XCTAssertEqual(r4.tx, 0, accuracy: 0.001) + XCTAssertEqual(r4.ty, 0, accuracy: 0.001) + + let p = Point(x: 1, y: 2).applying(rotation) + XCTAssertEqual(p.x, -2, accuracy: 0.001) + XCTAssertEqual(p.y, 1, accuracy: 0.001) + + let v = Vector(dx: 1, dy: 2).applying(rotation) + XCTAssertEqual(v.dx, -2, accuracy: 0.001) + XCTAssertEqual(v.dy, 1, accuracy: 0.001) + } +}