From 14a650efdf58b4a522d440ab5a3d7cd104679e75 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 16 Dec 2025 23:42:56 +0800 Subject: [PATCH 1/5] Implement _CGPathCopyDescription API --- Sources/OpenSwiftUICore/Shape/Path.swift | 7 +- .../Overlay/CoreGraphics/CGPath+OpenSwiftUI.h | 33 +++++++ .../Overlay/CoreGraphics/CGPath+OpenSwiftUI.m | 88 +++++++++++++++++++ .../CoreGraphics/__CGPathParseString.h | 24 ----- .../CoreGraphics/__CGPathParseString.m | 12 --- .../CGPath+OpenSwiftUITests.swift | 53 +++++++++++ 6 files changed, 175 insertions(+), 42 deletions(-) create mode 100644 Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h create mode 100644 Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m delete mode 100644 Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/__CGPathParseString.h delete mode 100644 Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/__CGPathParseString.m create mode 100644 Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift diff --git a/Sources/OpenSwiftUICore/Shape/Path.swift b/Sources/OpenSwiftUICore/Shape/Path.swift index 5e695a045..552b1ecef 100644 --- a/Sources/OpenSwiftUICore/Shape/Path.swift +++ b/Sources/OpenSwiftUICore/Shape/Path.swift @@ -12,11 +12,6 @@ package import OpenRenderBoxShims import OpenSwiftUI_SPI public import OpenCoreGraphicsShims -#if canImport(CoreGraphics) -@_silgen_name("__CGPathParseString") -private func __CGPathParseString(_ path: CGMutablePath, _ utf8CString: UnsafePointer) -> Bool -#endif - // MARK: - Path /// The outline of a 2D shape. @@ -280,7 +275,7 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable { guard let str = nsString.utf8String else { return nil } - guard __CGPathParseString(mutablePath, str) else { + guard _CGPathParseString(mutablePath, str) else { return nil } storage = .path(PathBox(mutablePath)) diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h new file mode 100644 index 000000000..52754b9f3 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h @@ -0,0 +1,33 @@ +// +// CGPath+OpenSwiftUI.h +// OpenSwiftUI_SPI + +#ifndef CGPath_OpenSwiftUI_h +#define CGPath_OpenSwiftUI_h + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#include +#include + +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString); + +/// Creates a string description of a path with optional coordinate rounding. +/// +/// - Parameters: +/// - path: The path to describe. +/// - step: The rounding step for coordinates. When non-zero, coordinates +/// are rounded to the nearest multiple of this value. Pass 0 for no rounding. +/// - Returns: A string representation of the path using SVG-like commands +/// (m for move, l for line, h for close). +NSString * _CGPathCopyDescription(CGPathRef path, CGFloat step); + +OPENSWIFTUI_ASSUME_NONNULL_END + +#endif + +#endif /* CGPath_OpenSwiftUI_h */ diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m new file mode 100644 index 000000000..903b428ad --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m @@ -0,0 +1,88 @@ +// +// CGPath+OpenSwiftUI.m +// OpenSwiftUI_SPI + +#import "CGPath+OpenSwiftUI.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import +#import +#import + +BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString) { + // TODO + return NO; +} + +typedef struct PathInfo { + CFMutableStringRef description; + CGFloat step; + CGFloat inverseStep; +} PathInfo; + +#define APPEND_COORD(coord) do { \ + CGFloat value = (coord); \ + if (path_info->step != 0.0) { \ + value = path_info->step * round(value * path_info->inverseStep); \ + } \ + char buffer[64]; \ + snprintf_l(buffer, 64, NULL, "%g ", value); \ + CFStringAppendCString(path_info->description, buffer, kCFStringEncodingUTF8); \ +} while (0) + +#define APPEND_POINTS(count) do { \ + for (int i = 0; i < (count); i++) { \ + APPEND_COORD(element->points[i].x); \ + APPEND_COORD(element->points[i].y); \ + } \ +} while (0) + +void copy_path_iter(void * __nullable info, const CGPathElement * element) { + PathInfo *path_info = (PathInfo *)info; + if (path_info->description != NULL) { + CFStringAppend(path_info->description, CFSTR(" ")); + } + UniChar ch; + switch (element->type) { + case kCGPathElementMoveToPoint: + APPEND_POINTS(1); + ch = 'm'; + break; + case kCGPathElementAddLineToPoint: + APPEND_POINTS(1); + ch = 'l'; + break; + case kCGPathElementAddQuadCurveToPoint: + APPEND_POINTS(2); + ch = 'q'; + break; + case kCGPathElementAddCurveToPoint: + APPEND_POINTS(3); + ch = 'c'; + break; + case kCGPathElementCloseSubpath: + ch = 'h'; + break; + default: + return; + } + CFStringAppendCharacters(path_info->description, &ch, 1); +} + +#undef APPEND_COORD +#undef APPEND_POINTS + +NSString * _CGPathCopyDescription(CGPathRef path, CGFloat step) { + PathInfo info = { + CFStringCreateMutable(kCFAllocatorDefault, 0), + step, + 1.0 / step + }; + CGPathApply(path, &info, ©_path_iter); + return (__bridge_transfer NSString *)(info.description); +} + +//_CGPathCreateRoundedRect + +#endif diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/__CGPathParseString.h b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/__CGPathParseString.h deleted file mode 100644 index 0aa5e572b..000000000 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/__CGPathParseString.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// __CGPathParseString.h -// OpenSwiftUI_SPI - -#ifndef __CGPathParseString_h -#define __CGPathParseString_h - -#include "OpenSwiftUIBase.h" - -#if OPENSWIFTUI_TARGET_OS_DARWIN - -#include -#include - -OPENSWIFTUI_ASSUME_NONNULL_BEGIN - -OPENSWIFTUI_EXPORT -BOOL __CGPathParseString(CGMutablePathRef path, const char *utf8CString); - -OPENSWIFTUI_ASSUME_NONNULL_END - -#endif - -#endif /* __CGPathParseString_h */ diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/__CGPathParseString.m b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/__CGPathParseString.m deleted file mode 100644 index f3858f2ae..000000000 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/__CGPathParseString.m +++ /dev/null @@ -1,12 +0,0 @@ -// -// __CGPathParseString.m -// OpenSwiftUI_SPI - -#import "__CGPathParseString.h" - -#if OPENSWIFTUI_TARGET_OS_DARWIN -BOOL __CGPathParseString(CGMutablePathRef path, const char *utf8CString) { - // TODO - return NO; -} -#endif diff --git a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift new file mode 100644 index 000000000..c76ee709d --- /dev/null +++ b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift @@ -0,0 +1,53 @@ +// +// CGPath+OpenSwiftUITests.swift +// OpenSwiftUI_SPITests + +import OpenSwiftUI_SPI +import Testing + +#if canImport(Darwin) +import CoreGraphics + +@Suite +struct CGPath_OpenSwiftUITests { + // MARK: - _CGPathCopyDescription + + struct CopyDescription { + @Test(arguments: [ + (1.0, " 100 0 m 190 190 l 0 190 l h"), + (2.0, " 100 0 m 190 190 l 0 190 l h"), + (3.0, " 99 0 m 189 189 l 0 189 l h"), + (4.0, " 100 0 m 188 188 l 0 188 l h"), + ]) + func copyDescription(step: Double, expected: String) { + let path = CGMutablePath() + path.move(to: CGPoint(x: 100, y: 0)) + path.addLine(to: CGPoint(x: 189.5, y: 189.5)) + path.addLine(to: CGPoint(x: 0, y: 189.5)) + path.closeSubpath() + let description = _CGPathCopyDescription(path, step) + #expect(description == expected) + } + + @Test + func copyDescriptionWithQuadCurve() { + let path = CGMutablePath() + path.move(to: CGPoint(x: 0, y: 0)) + path.addQuadCurve(to: CGPoint(x: 100, y: 100), control: CGPoint(x: 50, y: 0)) + let description = _CGPathCopyDescription(path, 1) + #expect(description == " 0 0 m 50 0 100 100 q") + } + + @Test + func copyDescriptionWithCubicCurve() { + let path = CGMutablePath() + path.move(to: CGPoint(x: 0, y: 0)) + path.addCurve(to: CGPoint(x: 100, y: 100), control1: CGPoint(x: 25, y: 0), control2: CGPoint(x: 75, y: 100)) + let description = _CGPathCopyDescription(path, 1) + #expect(description == " 0 0 m 25 0 75 100 100 100 c") + } + } +} + +#endif + From 92bf4e7bc0b8d162c6b3dae55ae647eccdf759b7 Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 17 Dec 2025 00:10:29 +0800 Subject: [PATCH 2/5] Add _CGPathParseString API --- .../Overlay/CoreGraphics/CGPath+OpenSwiftUI.h | 17 ++ .../Overlay/CoreGraphics/CGPath+OpenSwiftUI.m | 163 +++++++++++++++++- .../CGPath+OpenSwiftUITests.swift | 39 +++++ 3 files changed, 217 insertions(+), 2 deletions(-) diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h index 52754b9f3..9688287c7 100644 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h @@ -14,6 +14,23 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN +/// Parses a path string and appends the path elements to a mutable path. +/// +/// The string format uses space-separated numbers followed by command characters: +/// - `m` - move to (x y) +/// - `l` - line to (x y) +/// - `c` - cubic curve (cp1x cp1y cp2x cp2y x y) +/// - `q` - quad curve (cpx cpy x y) +/// - `t` - smooth quad curve (x y), reflects previous control point +/// - `v` - smooth cubic curve (cp2x cp2y x y), uses current point as cp1 +/// - `y` - shorthand cubic (cp1x cp1y x y), cp2 equals endpoint +/// - `h` - close subpath +/// - `re` - rectangle (x y width height) +/// +/// - Parameters: +/// - path: The mutable path to append elements to. +/// - utf8CString: The path string to parse. +/// - Returns: `YES` if parsing succeeded, `NO` if the string is malformed. BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString); /// Creates a string description of a path with optional coordinate rounding. diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m index 903b428ad..a6322c0f2 100644 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m @@ -10,9 +10,168 @@ #import #import +// NOTE: Not audited yet. Use with caution. BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString) { - // TODO - return NO; + if (path == NULL || utf8CString == NULL) { + return NO; + } + + CGFloat numbers[6]; + int numCount = 0; + CGFloat currentX = 0.0, currentY = 0.0; + CGFloat lastControlX = 0.0, lastControlY = 0.0; + + const char *ptr = utf8CString; + + while (YES) { + // Skip whitespace (characters <= 0x1f or space 0x20) + while (*ptr != '\0' && (*ptr <= 0x1f || *ptr == ' ')) { + ptr++; + } + + if (*ptr == '\0') { + // End of string - success if all numbers consumed + return numCount == 0; + } + + char c = *ptr; + + // Check if character can start a number + // Valid: digits 0-9 (0x30-0x39), '-', '+', '.', 'e', 'E', 'p', 'x', "inf" + BOOL isNumberStart = NO; + if ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.') { + isNumberStart = YES; + } else if (c == 'e' || c == 'E' || c == 'p' || c == 'x') { + isNumberStart = YES; + } else if (c == 'i') { + // Check for "inf" + if (ptr[1] == 'n' && ptr[2] == 'f') { + isNumberStart = YES; + } + } + + if (isNumberStart) { + if (numCount >= 6) { + return NO; + } + char *endPtr; + numbers[numCount++] = strtod_l(ptr, &endPtr, NULL); + ptr = endPtr; + continue; + } + + // Process command character + switch (c) { + case 'm': + if (numCount != 2) return NO; + CGPathMoveToPoint(path, NULL, numbers[0], numbers[1]); + lastControlX = currentX = numbers[0]; + lastControlY = currentY = numbers[1]; + numCount = 0; + break; + + case 'l': + if (numCount != 2) return NO; + CGPathAddLineToPoint(path, NULL, numbers[0], numbers[1]); + lastControlX = currentX = numbers[0]; + lastControlY = currentY = numbers[1]; + numCount = 0; + break; + + case 'c': + if (numCount != 6) return NO; + CGPathAddCurveToPoint(path, NULL, + numbers[0], numbers[1], + numbers[2], numbers[3], + numbers[4], numbers[5]); + lastControlX = numbers[2]; + lastControlY = numbers[3]; + currentX = numbers[4]; + currentY = numbers[5]; + numCount = 0; + break; + + case 'q': + if (numCount != 4) return NO; + CGPathAddQuadCurveToPoint(path, NULL, + numbers[0], numbers[1], + numbers[2], numbers[3]); + lastControlX = numbers[0]; + lastControlY = numbers[1]; + currentX = numbers[2]; + currentY = numbers[3]; + numCount = 0; + break; + + case 't': + // Smooth quad curve: reflect last control point + if (numCount != 2) return NO; + { + CGFloat reflectedX = currentX - 2.0 * lastControlX; + CGFloat reflectedY = currentY - 2.0 * lastControlY; + CGPathAddQuadCurveToPoint(path, NULL, + reflectedX, reflectedY, + numbers[0], numbers[1]); + lastControlX = reflectedX; + lastControlY = reflectedY; + currentX = numbers[0]; + currentY = numbers[1]; + } + numCount = 0; + break; + + case 'v': + // Smooth cubic curve: use current point as cp1 + if (numCount != 4) return NO; + CGPathAddCurveToPoint(path, NULL, + currentX, currentY, + numbers[0], numbers[1], + numbers[2], numbers[3]); + lastControlX = numbers[0]; + lastControlY = numbers[1]; + currentX = numbers[2]; + currentY = numbers[3]; + numCount = 0; + break; + + case 'y': + // Shorthand cubic: cp2 = endpoint + if (numCount != 4) return NO; + CGPathAddCurveToPoint(path, NULL, + numbers[0], numbers[1], + numbers[2], numbers[3], + numbers[2], numbers[3]); + currentX = numbers[2]; + currentY = numbers[3]; + numCount = 0; + break; + + case 'h': + if (numCount != 0) return NO; + CGPathCloseSubpath(path); + lastControlY = 0.0; + numCount = 0; + break; + + case 'r': + // Check for "re" (rectangle) + if (ptr[1] == 'e') { + if (numCount != 4) return NO; + CGPathAddRect(path, NULL, CGRectMake(numbers[0], numbers[1], + numbers[2], numbers[3])); + currentX = numbers[0]; + currentY = numbers[1]; + numCount = 0; + ptr++; // Skip 'e' + break; + } + return NO; + + default: + return NO; + } + ptr++; + } } typedef struct PathInfo { diff --git a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift index c76ee709d..40e3e61db 100644 --- a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift +++ b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift @@ -10,6 +10,45 @@ import CoreGraphics @Suite struct CGPath_OpenSwiftUITests { + // MARK: - _CGPathParseString + + struct ParseString { + @Test(arguments: [ + // Basic commands: move, line, close + ("100 0 m 200 100 l h", " 100 0 m 200 100 l h"), + // Multiple lines + ("0 0 m 100 0 l 100 100 l 0 100 l h", " 0 0 m 100 0 l 100 100 l 0 100 l h"), + // Quad curve + ("0 0 m 50 0 100 100 q", " 0 0 m 50 0 100 100 q"), + // Cubic curve + ("0 0 m 25 0 75 100 100 100 c", " 0 0 m 25 0 75 100 100 100 c"), + // Rectangle + ("10 20 30 40 re", " 10 20 m 40 20 l 40 60 l 10 60 l h"), + // Negative numbers + ("-10 -20 m 30 40 l", " -10 -20 m 30 40 l"), + // Decimal numbers + ("0.5 1.5 m 2.5 3.5 l", " 0.5 1.5 m 2.5 3.5 l"), + ]) + func parseString(input: String, expected: String) { + let path = CGMutablePath() + let result = _CGPathParseString(path, input) + #expect(result == true) + let description = _CGPathCopyDescription(path, 0) + #expect(description == expected) + } + + @Test + func parseStringWithInvalidInput() { + let path = CGMutablePath() + // Unknown command + #expect(_CGPathParseString(path, "0 0 z") == false) + // Wrong number of parameters for move + #expect(_CGPathParseString(path, "0 m") == false) + // Too many numbers without command + #expect(_CGPathParseString(path, "1 2 3 4 5 6 7 m") == false) + } + } + // MARK: - _CGPathCopyDescription struct CopyDescription { From 7e68498368627f510fc22433197d2e2898bb243c Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 17 Dec 2025 00:36:00 +0800 Subject: [PATCH 3/5] Add _CGPathCreateRoundedRect --- Package.swift | 8 +++ .../Overlay/CoreGraphics/CGPath+OpenSwiftUI.h | 17 ++++++ .../Overlay/CoreGraphics/CGPath+OpenSwiftUI.m | 59 ++++++++++++++++++- .../CGPath+OpenSwiftUITests.swift | 37 ++++++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index c31e4eec1..c791383c9 100644 --- a/Package.swift +++ b/Package.swift @@ -392,6 +392,14 @@ extension Target { var swiftSettings = swiftSettings ?? [] swiftSettings.append(.define("OPENRENDERBOX_RENDERBOX")) self.swiftSettings = swiftSettings + + var cSettings = cSettings ?? [] + cSettings.append(.define("OPENRENDERBOX_RENDERBOX")) + self.cSettings = cSettings + + var cxxSettings = cxxSettings ?? [] + cxxSettings.append(.define("OPENRENDERBOX_RENDERBOX")) + self.cxxSettings = cxxSettings } func addCoreUISettings() { diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h index 9688287c7..7eb7f1314 100644 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h @@ -43,6 +43,23 @@ BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString); /// (m for move, l for line, h for close). NSString * _CGPathCopyDescription(CGPathRef path, CGFloat step); +/// Creates a rounded rectangle path with the specified corner radii. +/// +/// The corner radii are automatically clamped to fit within the rectangle: +/// - Negative values are treated as 0 +/// - Values exceeding half the width or height are reduced accordingly +/// +/// - Parameters: +/// - rect: The rectangle to create the path from. +/// - cornerWidth: The horizontal radius of the rounded corners. +/// - cornerHeight: The vertical radius of the rounded corners. +/// - useRB: If `YES`, uses RenderBox for path creation (when available). +/// If `NO`, uses CoreGraphics directly. +/// - Returns: A new path representing the rounded rectangle. Returns a plain +/// rectangle path if either corner dimension is 0 or if the rect is empty. +CF_RETURNS_RETAINED +CGPathRef _CGPathCreateRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, BOOL useRB); + OPENSWIFTUI_ASSUME_NONNULL_END #endif diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m index a6322c0f2..1b95d0c63 100644 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m @@ -10,6 +10,12 @@ #import #import +#if OPENRENDERBOX_RENDERBOX +@import RenderBox; +#else +@import OpenRenderBox; +#endif + // NOTE: Not audited yet. Use with caution. BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString) { if (path == NULL || utf8CString == NULL) { @@ -242,6 +248,57 @@ void copy_path_iter(void * __nullable info, const CGPathElement * element) { return (__bridge_transfer NSString *)(info.description); } -//_CGPathCreateRoundedRect +CGPathRef _CGPathCreateRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, BOOL useRB) { + // Clamp corner dimensions to be non-negative + if (cornerWidth < 0.0) { + cornerWidth = 0.0; + } + if (cornerHeight < 0.0) { + cornerHeight = 0.0; + } + + // If either corner dimension is 0, or rect is empty, return a plain rectangle + if (cornerWidth == 0.0 || cornerHeight == 0.0 || CGRectIsEmpty(rect)) { + return CGPathCreateWithRect(rect, NULL); + } + + if (useRB) { + #if OPENRENDERBOX_RENDERBOX + // RBPath rbPath = RBPathMakeRoundedRect(NULL, rect, cornerWidth, cornerHeight, YES); + // CGPathRef cgPath = RBPathCopyCGPath(rbPath); + // RBPathRelease(rbPath); + // return cgPath; + #else + // ORBPath rbPath = ORBPathMakeRoundedRect(NULL, rect, cornerWidth, cornerHeight, YES); + // CGPathRef cgPath = ORBPathCopyCGPath(rbPath); + // ORBPathRelease(rbPath); + // return cgPath; + #endif + } + + // Use CoreGraphics path creation + CGFloat width = CGRectGetWidth(rect); + CGFloat height = CGRectGetHeight(rect); + + // Clamp cornerWidth to at most half the width + if (cornerWidth * 2.0 > width) { + cornerWidth = nextafter(width * 0.5, 0.0); + } + + // Clamp cornerHeight to at most half the height + if (cornerHeight * 2.0 > height) { + cornerHeight = nextafter(height * 0.5, 0.0); + } + + // Final validation + if (cornerWidth < 0.0 || cornerWidth * 2.0 > width) { + return CGPathCreateWithRect(rect, NULL); + } + if (cornerHeight < 0.0 || cornerHeight * 2.0 > height) { + return CGPathCreateWithRect(rect, NULL); + } + + return CGPathCreateWithRoundedRect(rect, cornerWidth, cornerHeight, NULL); +} #endif diff --git a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift index 40e3e61db..0baf8b41e 100644 --- a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift +++ b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift @@ -86,6 +86,43 @@ struct CGPath_OpenSwiftUITests { #expect(description == " 0 0 m 25 0 75 100 100 100 c") } } + + // MARK: - _CGPathCreateRoundedRect + + struct CreateRoundedRect { + @Test + func zeroCornerWidth() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let path = _CGPathCreateRoundedRect(rect, 0, 10, false) + // Should return a plain rectangle + #expect(_CGPathCopyDescription(path, 0) == " 0 0 m 100 0 l 100 50 l 0 50 l h") + } + + @Test + func zeroCornerHeight() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let path = _CGPathCreateRoundedRect(rect, 10, 0, false) + // Should return a plain rectangle + #expect(_CGPathCopyDescription(path, 0) == " 0 0 m 100 0 l 100 50 l 0 50 l h") + } + + @Test + func negativeCornerValues() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 50) + let path = _CGPathCreateRoundedRect(rect, -5, -5, false) + // Negative values are clamped to 0, so should return a plain rectangle + #expect(_CGPathCopyDescription(path, 0) == " 0 0 m 100 0 l 100 50 l 0 50 l h") + } + + @Test + func emptyRect() { + // A rect with zero width or height is considered empty by CGRectIsEmpty + let rect = CGRect(x: 0, y: 0, width: 0, height: 50) + let path = _CGPathCreateRoundedRect(rect, 10, 10, false) + // Empty rect returns a plain rectangle path + #expect(_CGPathCopyDescription(path, 0) == " 0 0 m 0 0 l 0 50 l 0 50 l h") + } + } } #endif From 8cce154402b71dd0168fb47376dd9943401b4292 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 20 Dec 2025 19:22:16 +0800 Subject: [PATCH 4/5] Audit _CGPathParseString implementation --- .../Overlay/CoreGraphics/CGPath+OpenSwiftUI.h | 24 ++- .../Overlay/CoreGraphics/CGPath+OpenSwiftUI.m | 172 +++++++----------- .../CGPath+OpenSwiftUITests.swift | 57 +++--- 3 files changed, 108 insertions(+), 145 deletions(-) diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h index 7eb7f1314..ab602bda2 100644 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h @@ -17,15 +17,21 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN /// Parses a path string and appends the path elements to a mutable path. /// /// The string format uses space-separated numbers followed by command characters: -/// - `m` - move to (x y) -/// - `l` - line to (x y) -/// - `c` - cubic curve (cp1x cp1y cp2x cp2y x y) -/// - `q` - quad curve (cpx cpy x y) -/// - `t` - smooth quad curve (x y), reflects previous control point -/// - `v` - smooth cubic curve (cp2x cp2y x y), uses current point as cp1 -/// - `y` - shorthand cubic (cp1x cp1y x y), cp2 equals endpoint -/// - `h` - close subpath -/// - `re` - rectangle (x y width height) +/// +/// | Command | Parameters | Description | +/// |---------|------------|-------------| +/// | `m` | x y | Move to point | +/// | `l` | x y | Line to point | +/// | `c` | cp1x cp1y cp2x cp2y x y | Cubic Bézier curve | +/// | `q` | cpx cpy x y | Quadratic Bézier curve | +/// | `t` | x y | Smooth quadratic curve (reflects previous control point) | +/// | `v` | cp2x cp2y x y | Smooth cubic curve (uses last point as cp1) | +/// | `y` | cp1x cp1y x y | Shorthand cubic (cp2 equals endpoint) | +/// | `h` | (none) | Close subpath | +/// | `re` | x y width height | Rectangle | +/// +/// Whitespace characters (space, tab, newline, carriage return) are skipped. +/// Numbers can be integers, decimals, or special values like `Inf`. /// /// - Parameters: /// - path: The mutable path to append elements to. diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m index 1b95d0c63..10ed1f960 100644 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m @@ -16,168 +16,122 @@ @import OpenRenderBox; #endif -// NOTE: Not audited yet. Use with caution. BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString) { - if (path == NULL || utf8CString == NULL) { - return NO; - } - - CGFloat numbers[6]; + double numbers[6]; int numCount = 0; CGFloat currentX = 0.0, currentY = 0.0; CGFloat lastControlX = 0.0, lastControlY = 0.0; - const char *ptr = utf8CString; - - while (YES) { - // Skip whitespace (characters <= 0x1f or space 0x20) - while (*ptr != '\0' && (*ptr <= 0x1f || *ptr == ' ')) { - ptr++; - } - - if (*ptr == '\0') { - // End of string - success if all numbers consumed - return numCount == 0; - } - - char c = *ptr; - - // Check if character can start a number - // Valid: digits 0-9 (0x30-0x39), '-', '+', '.', 'e', 'E', 'p', 'x', "inf" - BOOL isNumberStart = NO; - if ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.') { - isNumberStart = YES; - } else if (c == 'e' || c == 'E' || c == 'p' || c == 'x') { - isNumberStart = YES; - } else if (c == 'i') { - // Check for "inf" - if (ptr[1] == 'n' && ptr[2] == 'f') { - isNumberStart = YES; - } - } - - if (isNumberStart) { - if (numCount >= 6) { - return NO; + do { + while (*ptr <= 0x1f) { + switch (*ptr) { + case 0: return true; + case 9: case 10: case 12: case 13: ptr++; break; + default: return false; } - char *endPtr; - numbers[numCount++] = strtod_l(ptr, &endPtr, NULL); - ptr = endPtr; - continue; } - - // Process command character + unsigned char c = (unsigned char)*ptr; + BOOL isNumberStart = NO; switch (c) { + case ' ': break; + case '+': case '-': case '.': case '0' ... '9': + case 'E': case 'P': case 'X': case 'e': case 'p': case 'x': + isNumberStart = YES; + break; + case 'I': + if (ptr[1] == 'n' && ptr[2] == 'f') { isNumberStart = YES; } + break; case 'm': if (numCount != 2) return NO; CGPathMoveToPoint(path, NULL, numbers[0], numbers[1]); - lastControlX = currentX = numbers[0]; - lastControlY = currentY = numbers[1]; + currentX = lastControlX = numbers[0]; + currentY = lastControlY = numbers[1]; numCount = 0; break; - case 'l': if (numCount != 2) return NO; CGPathAddLineToPoint(path, NULL, numbers[0], numbers[1]); - lastControlX = currentX = numbers[0]; - lastControlY = currentY = numbers[1]; + currentX = lastControlX = numbers[0]; + currentY = lastControlY = numbers[1]; numCount = 0; break; - case 'c': if (numCount != 6) return NO; - CGPathAddCurveToPoint(path, NULL, - numbers[0], numbers[1], - numbers[2], numbers[3], - numbers[4], numbers[5]); - lastControlX = numbers[2]; - lastControlY = numbers[3]; - currentX = numbers[4]; - currentY = numbers[5]; + CGPathAddCurveToPoint(path, NULL, numbers[0], numbers[1], + numbers[2], numbers[3], numbers[4], numbers[5]); + currentX = numbers[2]; + currentY = numbers[3]; + lastControlX = numbers[4]; + lastControlY = numbers[5]; numCount = 0; break; - case 'q': if (numCount != 4) return NO; - CGPathAddQuadCurveToPoint(path, NULL, - numbers[0], numbers[1], + CGPathAddQuadCurveToPoint(path, NULL, numbers[0], numbers[1], numbers[2], numbers[3]); - lastControlX = numbers[0]; - lastControlY = numbers[1]; - currentX = numbers[2]; - currentY = numbers[3]; + currentX = numbers[0]; + currentY = numbers[1]; + lastControlX = numbers[2]; + lastControlY = numbers[3]; numCount = 0; break; - case 't': - // Smooth quad curve: reflect last control point if (numCount != 2) return NO; { CGFloat reflectedX = currentX - 2.0 * lastControlX; CGFloat reflectedY = currentY - 2.0 * lastControlY; - CGPathAddQuadCurveToPoint(path, NULL, - reflectedX, reflectedY, + CGPathAddQuadCurveToPoint(path, NULL, reflectedX, reflectedY, numbers[0], numbers[1]); - lastControlX = reflectedX; - lastControlY = reflectedY; - currentX = numbers[0]; - currentY = numbers[1]; + currentX = reflectedX; + currentY = reflectedY; + lastControlX = numbers[0]; + lastControlY = numbers[1]; } numCount = 0; break; - case 'v': - // Smooth cubic curve: use current point as cp1 if (numCount != 4) return NO; - CGPathAddCurveToPoint(path, NULL, - currentX, currentY, - numbers[0], numbers[1], - numbers[2], numbers[3]); - lastControlX = numbers[0]; - lastControlY = numbers[1]; - currentX = numbers[2]; - currentY = numbers[3]; + CGPathAddCurveToPoint(path, NULL, lastControlX, lastControlY, + numbers[0], numbers[1], numbers[2], numbers[3]); + lastControlX = numbers[2]; + lastControlY = numbers[3]; numCount = 0; break; - case 'y': - // Shorthand cubic: cp2 = endpoint if (numCount != 4) return NO; - CGPathAddCurveToPoint(path, NULL, - numbers[0], numbers[1], - numbers[2], numbers[3], - numbers[2], numbers[3]); - currentX = numbers[2]; - currentY = numbers[3]; + CGPathAddCurveToPoint(path, NULL, numbers[0], numbers[1], + numbers[2], numbers[3], numbers[2], numbers[3]); + lastControlX = numbers[2]; + lastControlY = numbers[3]; numCount = 0; break; - case 'h': if (numCount != 0) return NO; CGPathCloseSubpath(path); + lastControlX = 0.0; lastControlY = 0.0; numCount = 0; break; - case 'r': - // Check for "re" (rectangle) - if (ptr[1] == 'e') { - if (numCount != 4) return NO; - CGPathAddRect(path, NULL, CGRectMake(numbers[0], numbers[1], - numbers[2], numbers[3])); - currentX = numbers[0]; - currentY = numbers[1]; - numCount = 0; - ptr++; // Skip 'e' - break; - } - return NO; - + if (ptr[1] != 'e') return NO; + if (numCount != 4) return NO; + CGPathAddRect(path, NULL, CGRectMake(numbers[0], numbers[1], + numbers[2], numbers[3])); + ptr++; + numCount = 0; + break; default: - return NO; + return false; } - ptr++; - } + if (isNumberStart) { + if (numCount == 6) return false; + char *endPtr; + numbers[numCount++] = strtod_l(ptr, &endPtr, NULL); + ptr = endPtr; + } else { + ptr++; + } + } while (1); } typedef struct PathInfo { diff --git a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift index 0baf8b41e..55f218e3c 100644 --- a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift +++ b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift @@ -14,38 +14,41 @@ struct CGPath_OpenSwiftUITests { struct ParseString { @Test(arguments: [ - // Basic commands: move, line, close - ("100 0 m 200 100 l h", " 100 0 m 200 100 l h"), - // Multiple lines - ("0 0 m 100 0 l 100 100 l 0 100 l h", " 0 0 m 100 0 l 100 100 l 0 100 l h"), - // Quad curve - ("0 0 m 50 0 100 100 q", " 0 0 m 50 0 100 100 q"), - // Cubic curve - ("0 0 m 25 0 75 100 100 100 c", " 0 0 m 25 0 75 100 100 100 c"), - // Rectangle - ("10 20 30 40 re", " 10 20 m 40 20 l 40 60 l 10 60 l h"), + // m - move to + ("100 0 m 200 100 l h", true, " 100 0 m 200 100 l h"), + // l - line to (multiple) + ("0 0 m 100 0 l 100 100 l 0 100 l h", true, " 0 0 m 100 0 l 100 100 l 0 100 l h"), + // q - quad curve + ("0 0 m 50 0 100 100 q", true, " 0 0 m 50 0 100 100 q"), + // c - cubic curve + ("0 0 m 25 0 75 100 100 100 c", true, " 0 0 m 25 0 75 100 100 100 c"), + // v - smooth cubic (cp1 = last point) + ("0 0 m 50 50 100 100 v", true, " 0 0 m 0 0 50 50 100 100 c"), + // y - shorthand cubic (cp2 = endpoint) + ("0 0 m 25 0 100 100 y", true, " 0 0 m 25 0 100 100 100 100 c"), + // re - rectangle + ("10 20 30 40 re", true, " 10 20 m 40 20 l 40 60 l 10 60 l h"), // Negative numbers - ("-10 -20 m 30 40 l", " -10 -20 m 30 40 l"), + ("-10 -20 m 30 40 l", true, " -10 -20 m 30 40 l"), // Decimal numbers - ("0.5 1.5 m 2.5 3.5 l", " 0.5 1.5 m 2.5 3.5 l"), + ("0.5 1.5 m 2.5 3.5 l", true, " 0.5 1.5 m 2.5 3.5 l"), + // Whitespace variants (tab, newline) + ("0\t0\nm\n100\t100\tl", true, " 0 0 m 100 100 l"), + // Invalid: unknown command + ("0 0 z", false, ""), + // Invalid: wrong number of parameters + ("0 m", false, ""), + // Invalid: too many numbers + ("1 2 3 4 5 6 7 m", false, ""), ]) - func parseString(input: String, expected: String) { + func parseString(input: String, expectedResult: Bool, expectedString: String) { let path = CGMutablePath() let result = _CGPathParseString(path, input) - #expect(result == true) - let description = _CGPathCopyDescription(path, 0) - #expect(description == expected) - } - - @Test - func parseStringWithInvalidInput() { - let path = CGMutablePath() - // Unknown command - #expect(_CGPathParseString(path, "0 0 z") == false) - // Wrong number of parameters for move - #expect(_CGPathParseString(path, "0 m") == false) - // Too many numbers without command - #expect(_CGPathParseString(path, "1 2 3 4 5 6 7 m") == false) + #expect(result == expectedResult) + if expectedResult { + let description = _CGPathCopyDescription(path, 0) + #expect(description == expectedString) + } } } From 5c9ff288d89fc0d006089f5b59a951a73bf2e603 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 21 Dec 2025 15:14:06 +0800 Subject: [PATCH 5/5] Fix _CGPathParseString --- .../Overlay/CoreGraphics/CGPath+OpenSwiftUI.m | 73 +++++++++++-------- .../CGPath+OpenSwiftUITests.swift | 3 +- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m index 10ed1f960..797051594 100644 --- a/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m +++ b/Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m @@ -43,64 +43,74 @@ BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString) { break; case 'm': if (numCount != 2) return NO; - CGPathMoveToPoint(path, NULL, numbers[0], numbers[1]); + CGPathMoveToPoint(path, NULL, + numbers[0], numbers[1]); currentX = lastControlX = numbers[0]; currentY = lastControlY = numbers[1]; numCount = 0; break; case 'l': if (numCount != 2) return NO; - CGPathAddLineToPoint(path, NULL, numbers[0], numbers[1]); + CGPathAddLineToPoint(path, NULL, + numbers[0], numbers[1]); currentX = lastControlX = numbers[0]; currentY = lastControlY = numbers[1]; numCount = 0; break; case 'c': if (numCount != 6) return NO; - CGPathAddCurveToPoint(path, NULL, numbers[0], numbers[1], - numbers[2], numbers[3], numbers[4], numbers[5]); - currentX = numbers[2]; - currentY = numbers[3]; - lastControlX = numbers[4]; - lastControlY = numbers[5]; + CGPathAddCurveToPoint(path, NULL, + numbers[0], numbers[1], + numbers[2], numbers[3], + numbers[4], numbers[5]); + lastControlX = numbers[2]; + lastControlY = numbers[3]; + currentX = numbers[4]; + currentY = numbers[5]; numCount = 0; break; case 'q': if (numCount != 4) return NO; - CGPathAddQuadCurveToPoint(path, NULL, numbers[0], numbers[1], + CGPathAddQuadCurveToPoint(path, NULL, + numbers[0], numbers[1], numbers[2], numbers[3]); - currentX = numbers[0]; - currentY = numbers[1]; - lastControlX = numbers[2]; - lastControlY = numbers[3]; + lastControlX = numbers[0]; + lastControlY = numbers[1]; + currentX = numbers[2]; + currentY = numbers[3]; numCount = 0; break; case 't': if (numCount != 2) return NO; - { - CGFloat reflectedX = currentX - 2.0 * lastControlX; - CGFloat reflectedY = currentY - 2.0 * lastControlY; - CGPathAddQuadCurveToPoint(path, NULL, reflectedX, reflectedY, - numbers[0], numbers[1]); - currentX = reflectedX; - currentY = reflectedY; - lastControlX = numbers[0]; - lastControlY = numbers[1]; - } + CGFloat reflectedX = currentX * 2.0 - lastControlX; + CGFloat reflectedY = currentY * 2.0 - lastControlY; + CGPathAddQuadCurveToPoint(path, NULL, + reflectedX, reflectedY, + numbers[0], numbers[1]); + lastControlX = reflectedX; + lastControlY = reflectedY; + currentX = numbers[0]; + currentY = numbers[1]; numCount = 0; break; case 'v': if (numCount != 4) return NO; - CGPathAddCurveToPoint(path, NULL, lastControlX, lastControlY, - numbers[0], numbers[1], numbers[2], numbers[3]); - lastControlX = numbers[2]; - lastControlY = numbers[3]; + CGPathAddCurveToPoint(path, NULL, + currentX, currentY, + numbers[0], numbers[1], + numbers[2], numbers[3]); + lastControlX = numbers[0]; + lastControlY = numbers[1]; + currentX = numbers[2]; + currentY = numbers[3]; numCount = 0; break; case 'y': if (numCount != 4) return NO; - CGPathAddCurveToPoint(path, NULL, numbers[0], numbers[1], - numbers[2], numbers[3], numbers[2], numbers[3]); + CGPathAddCurveToPoint(path, NULL, + numbers[0], numbers[1], + numbers[2], numbers[3], + numbers[2], numbers[3]); lastControlX = numbers[2]; lastControlY = numbers[3]; numCount = 0; @@ -115,8 +125,9 @@ BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString) { case 'r': if (ptr[1] != 'e') return NO; if (numCount != 4) return NO; - CGPathAddRect(path, NULL, CGRectMake(numbers[0], numbers[1], - numbers[2], numbers[3])); + CGPathAddRect(path, NULL, + CGRectMake(numbers[0], numbers[1], + numbers[2], numbers[3])); ptr++; numCount = 0; break; diff --git a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift index 55f218e3c..79b58b22c 100644 --- a/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift +++ b/Tests/OpenSwiftUI_SPITests/Overlay/CoreGraphics/CGPath+OpenSwiftUITests.swift @@ -22,7 +22,7 @@ struct CGPath_OpenSwiftUITests { ("0 0 m 50 0 100 100 q", true, " 0 0 m 50 0 100 100 q"), // c - cubic curve ("0 0 m 25 0 75 100 100 100 c", true, " 0 0 m 25 0 75 100 100 100 c"), - // v - smooth cubic (cp1 = last point) + // v - smooth cubic (cp1 = current point) ("0 0 m 50 50 100 100 v", true, " 0 0 m 0 0 50 50 100 100 c"), // y - shorthand cubic (cp2 = endpoint) ("0 0 m 25 0 100 100 y", true, " 0 0 m 25 0 100 100 100 100 c"), @@ -56,6 +56,7 @@ struct CGPath_OpenSwiftUITests { struct CopyDescription { @Test(arguments: [ + (0.0, " 100 0 m 189.5 189.5 l 0 189.5 l h"), (1.0, " 100 0 m 190 190 l 0 190 l h"), (2.0, " 100 0 m 190 190 l 0 190 l h"), (3.0, " 99 0 m 189 189 l 0 189 l h"),