From 9c24d75b703a43efd8961cfb93b87b44328a6ab1 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 2 Nov 2026 01:47:38 +0800 Subject: [PATCH 1/2] fix(geometry): support CGRect operations without CoreGraphics --- .../OpenCoreGraphics/CGRect+Extension.swift | 53 ++++++++++-- .../CGRectTests.swift | 82 +++++++++++++++++++ 2 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 Tests/OpenCoreGraphicsShimsTests/CGRectTests.swift diff --git a/Sources/OpenCoreGraphics/CGRect+Extension.swift b/Sources/OpenCoreGraphics/CGRect+Extension.swift index 24ae625..75d4781 100644 --- a/Sources/OpenCoreGraphics/CGRect+Extension.swift +++ b/Sources/OpenCoreGraphics/CGRect+Extension.swift @@ -11,30 +11,71 @@ extension CGRect { } public var width: CGFloat { - return size.width + return abs(size.width) } public var height: CGFloat { - return size.height + return abs(size.height) } public var minX: CGFloat { - return origin.x + return min(origin.x, origin.x + size.width) } public var minY: CGFloat { - return origin.y + return min(origin.y, origin.y + size.height) } public var maxX: CGFloat { - return origin.x + size.width + return max(origin.x, origin.x + size.width) } public var maxY: CGFloat { - return origin.y + size.height + return max(origin.y, origin.y + size.height) + } + + public var midX: CGFloat { + return origin.x + size.width / 2 + } + + public var midY: CGFloat { + return origin.y + size.height / 2 + } + + public var isNull: Bool { + return origin.x == .infinity || origin.y == .infinity + } + + public var standardized: CGRect { + guard !isNull else { return .null } + return CGRect(x: minX, y: minY, width: width, height: height) + } + + public func contains(_ point: CGPoint) -> Bool { + return !isNull && point.x >= minX && point.x < maxX + && point.y >= minY && point.y < maxY + } + + public func intersects(_ rect: CGRect) -> Bool { + guard !isNull && !rect.isNull else { return false } + func overlaps(_ start: CGFloat, _ end: CGFloat, _ otherStart: CGFloat, _ otherEnd: CGFloat) -> Bool { + if start == otherStart { return true } + return start < otherStart ? otherStart < end : start < otherEnd + } + return overlaps(minX, maxX, rect.minX, rect.maxX) + && overlaps(minY, maxY, rect.minY, rect.maxY) + } + + public func union(_ rect: CGRect) -> CGRect { + if isNull { return rect } + if rect.isNull { return self } + let x = min(minX, rect.minX) + let y = min(minY, rect.minY) + return CGRect(x: x, y: y, width: max(maxX, rect.maxX) - x, height: max(maxY, rect.maxY) - y) } public static let zero = CGRect(origin: .zero, size: .zero) + public static let null = CGRect(x: CGFloat.infinity, y: CGFloat.infinity, width: 0, height: 0) } extension CGRect: Swift.Hashable { diff --git a/Tests/OpenCoreGraphicsShimsTests/CGRectTests.swift b/Tests/OpenCoreGraphicsShimsTests/CGRectTests.swift new file mode 100644 index 0000000..63b4f0e --- /dev/null +++ b/Tests/OpenCoreGraphicsShimsTests/CGRectTests.swift @@ -0,0 +1,82 @@ +// +// CGRectTests.swift +// OpenCoreGraphicsShimsTests + +import OpenCoreGraphicsShims +import Testing + +struct CGRectTests { + @Test + func negativeSizeBounds() { + let rect = CGRect(x: 11, y: 22, width: -10, height: -20) + #expect(rect.width == 10) + #expect(rect.height == 20) + #expect(rect.minX == 1) + #expect(rect.midX == 6) + #expect(rect.maxX == 11) + #expect(rect.minY == 2) + #expect(rect.midY == 12) + #expect(rect.maxY == 22) + #expect(rect.standardized == CGRect(x: 1, y: 2, width: 10, height: 20)) + } + + @Test + func nullRectangles() { + #expect(CGRect.null.isNull) + #expect(!CGRect.zero.isNull) + #expect(CGRect.null.standardized == .null) + let rect = CGRect(x: CGFloat.infinity, y: 2, width: 10, height: 20) + #expect(rect.isNull) + #expect(rect.standardized == .null) + #expect(!CGRect(x: -CGFloat.infinity, y: 2, width: 10, height: 20).isNull) + } + + @Test(arguments: [ + (CGPoint(x: 0, y: 0), true), + (CGPoint(x: 5, y: 5), true), + (CGPoint(x: 10, y: 5), false), + (CGPoint(x: 5, y: 10), false), + (CGPoint(x: -1, y: 5), false), + ]) + func containsPoint(point: CGPoint, expected: Bool) { + let rect = CGRect(x: 10, y: 10, width: -10, height: -10) + #expect(rect.contains(point) == expected) + #expect(!CGRect.null.contains(point)) + #expect(!CGRect.zero.contains(point)) + } + + @Test(arguments: [ + (CGRect(x: 5, y: 5, width: 10, height: 10), true), + (CGRect(x: 10, y: 0, width: 10, height: 10), false), + (CGRect(x: 5, y: 5, width: 0, height: 0), true), + (CGRect(x: 5, y: 5, width: 0, height: 2), true), + (CGRect(x: 10, y: 5, width: 0, height: 0), false), + (CGRect.zero, true), + (CGRect.null, false), + ]) + func intersectionBounds(other: CGRect, expected: Bool) { + let rect = CGRect(x: 10, y: 10, width: -10, height: -10) + #expect(rect.intersects(other) == expected) + #expect(other.intersects(rect) == expected) + #expect(!CGRect.null.intersects(other)) + } + + @Test + func unionIncludesDegenerateBounds() { + let rect = CGRect(x: 10, y: 10, width: -10, height: -10) + #expect(rect.union(.null) == rect) + #expect(CGRect.null.union(rect) == rect) + #expect(CGRect.null.union(.null) == .null) + let point = CGRect(x: 15, y: 20, width: 0, height: 0) + #expect(CGRect.zero.union(point) == CGRect(x: 0, y: 0, width: 15, height: 20)) + #expect(rect.union(point) == CGRect(x: 0, y: 0, width: 15, height: 20)) + } + + @Test + func coincidentDegenerateIntersections() { + #expect(CGRect.zero.intersects(.zero)) + let line = CGRect(x: 0, y: 0, width: 0, height: 10) + #expect(line.intersects(CGRect(x: 0, y: 2, width: 0, height: 4))) + #expect(!line.intersects(CGRect(x: 1, y: 2, width: 0, height: 4))) + } +} From df291e64f36ba9d0483fd1687caf73ef7262399e Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 7 Nov 2026 18:15:06 +0800 Subject: [PATCH 2/2] test: account for CGRect intersection differences --- .../CGRectTests.swift | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Tests/OpenCoreGraphicsShimsTests/CGRectTests.swift b/Tests/OpenCoreGraphicsShimsTests/CGRectTests.swift index 63b4f0e..94bb8b5 100644 --- a/Tests/OpenCoreGraphicsShimsTests/CGRectTests.swift +++ b/Tests/OpenCoreGraphicsShimsTests/CGRectTests.swift @@ -47,10 +47,9 @@ struct CGRectTests { @Test(arguments: [ (CGRect(x: 5, y: 5, width: 10, height: 10), true), - (CGRect(x: 10, y: 0, width: 10, height: 10), false), + (CGRect(x: 11, y: 0, width: 10, height: 10), false), (CGRect(x: 5, y: 5, width: 0, height: 0), true), (CGRect(x: 5, y: 5, width: 0, height: 2), true), - (CGRect(x: 10, y: 5, width: 0, height: 0), false), (CGRect.zero, true), (CGRect.null, false), ]) @@ -61,6 +60,24 @@ struct CGRectTests { #expect(!CGRect.null.intersects(other)) } + @Test(arguments: [ + CGRect(x: 10, y: 0, width: 10, height: 10), + CGRect(x: 10, y: 5, width: 0, height: 0), + ]) + func maximumEdgeIntersection(other: CGRect) { + let rect = CGRect(x: 10, y: 10, width: -10, height: -10) + // OpenCoreGraphics follows Darwin CoreGraphics for boundary intersections. + // Linux currently uses Swift Foundation's CGRect, which includes these + // boundary contacts. The Linux expectation records this platform difference. + #if canImport(Darwin) + let expected = false + #else + let expected = true + #endif + #expect(rect.intersects(other) == expected) + #expect(other.intersects(rect) == expected) + } + @Test func unionIncludesDegenerateBounds() { let rect = CGRect(x: 10, y: 10, width: -10, height: -10)