diff --git a/Package.resolved b/Package.resolved index f44abb3..9d71347 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "afb2d7d71fc5958731e2203564551e6c4fd33689a5df8057159b3b0e0ab9762d", + "originHash" : "1477c74d879eeac85b29c7bc660a95d816c9b92adc30152ebfdd49d35535b059", "pins" : [ { "identity" : "darwinprivateframeworks", @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "bc86728a45d2d6dc04bea9ad248971b24c36f019" + "revision" : "511cc728141f57dc325753f23898568ccd49c339" } }, { @@ -16,7 +16,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenCoreGraphics", "state" : { "branch" : "main", - "revision" : "0d7a0d0d9e236cbf9dea2f10a5b0b88c8789d9e4" + "revision" : "de4866d03ab727b5204633a2c29cb1412dd90ee6" } }, { diff --git a/Package.swift b/Package.swift index 6387a61..e926f83 100644 --- a/Package.swift +++ b/Package.swift @@ -155,12 +155,11 @@ let compatibilityTestCondition = envBoolValue("COMPATIBILITY_TEST", default: fal let useLocalDeps = envBoolValue("USE_LOCAL_DEPS") let renderBoxCondtion = envBoolValue("RENDERBOX", default: buildForDarwinPlatform && !isSPIBuild) -/// CGFloat and CGRect def in CFCGTypes.h will conflict with Foundation's CGSize/CGRect def on Linux. -/// macOS: true -> no issue -/// macOS: false -> use Swift implementation with OpenCoreGraphics Swift CGPath -/// Linux: true + No CGPathRef support in ORBPath -> confilict with Foundation def -/// Linux: false -> use Swift implementation with OpenCoreGraphics Swift CGPath +// C geometry types conflict with Foundation's Swift geometry types on Linux. +// Disable this flag to use Swift Path instead of the C++ implementation. let cfCGTypes = envBoolValue("CF_CGTYPES", default: buildForDarwinPlatform) +// Use OPENSWIFTUI_COREGRAPHICS to select the same backend in ORB and OCG shims. +let coreGraphicsCondition = envBoolValue("COREGRAPHICS", default: buildForDarwinPlatform) // MARK: - Shared Settings @@ -190,6 +189,10 @@ if cfCGTypes { sharedSwiftSettings.append(.define("OPENRENDERBOX_CF_CGTYPES")) } +if coreGraphicsCondition { + sharedSwiftSettings.append(.define("OPENRENDERBOX_COREGRAPHICS")) +} + // MARK: - Extension extension Target { diff --git a/Sources/OpenRenderBox/Animation/ORBSymbolUpdate.swift b/Sources/OpenRenderBox/Animation/ORBSymbolUpdate.swift index 5b468b4..9ea0392 100644 --- a/Sources/OpenRenderBox/Animation/ORBSymbolUpdate.swift +++ b/Sources/OpenRenderBox/Animation/ORBSymbolUpdate.swift @@ -4,7 +4,7 @@ #if !canImport(ObjectiveC) || !OPENRENDERBOX_CF_CGTYPES public import OpenCoreGraphicsShims -public import OpenRenderBoxCxx +public import OpenRenderBoxCxx.C #endif #if !canImport(ObjectiveC) diff --git a/Sources/OpenRenderBox/Export.swift b/Sources/OpenRenderBox/Export.swift index 4bfb9e2..6a26528 100644 --- a/Sources/OpenRenderBox/Export.swift +++ b/Sources/OpenRenderBox/Export.swift @@ -3,5 +3,12 @@ // OpenRenderBox #if SWIFT_PACKAGE +#if OPENRENDERBOX_COREGRAPHICS @_exported public import OpenRenderBoxCxx +#if canImport(ObjectiveC) +@_exported public import OpenRenderBoxCxx.ObjC +#endif +#else +@_exported public import OpenRenderBoxCxx.C +#endif #endif diff --git a/Sources/OpenRenderBox/Path/ORBPath.swift b/Sources/OpenRenderBox/Path/ORBPath.swift index 5242128..713cd26 100644 --- a/Sources/OpenRenderBox/Path/ORBPath.swift +++ b/Sources/OpenRenderBox/Path/ORBPath.swift @@ -6,6 +6,162 @@ public import OpenCoreGraphicsShims public typealias ORBPathApplyCallback = (UnsafeMutableRawPointer, ORBPath.Element, UnsafePointer, UnsafeRawPointer?) -> Bool +public typealias ORBPathApplyShapeCallback = (UnsafeMutableRawPointer, ORBPathShapeType, UnsafePointer) -> Void + +public enum ORBPathShapeType: Int32, @unchecked Sendable { + case rect = 0 + case ellipse = 1 + case line = 2 + case fixedRoundedRect = 3 + case unevenRoundedRect = 4 + case nested = 5 + case stroked = 6 + case trimmed = 7 + case offset = 8 +} + +public struct ORBPathRectShape: @unchecked Sendable { + public var transform: CGAffineTransform + public var rect: CGRect + + public init(transform: CGAffineTransform, rect: CGRect) { + self.transform = transform + self.rect = rect + } +} + +public struct ORBPathFixedRoundedRectShape: @unchecked Sendable { + public var transform: CGAffineTransform + public var rect: CGRect + public var cornerSize: CGSize + public var style: ORBPath.RoundedCornerStyle + + public init(transform: CGAffineTransform, rect: CGRect, cornerSize: CGSize, style: ORBPath.RoundedCornerStyle) { + self.transform = transform + self.rect = rect + self.cornerSize = cornerSize + self.style = style + } +} + +public struct ORBPathUnevenRoundedRectShape: @unchecked Sendable { + public var transform: CGAffineTransform + public var rect: CGRect + public var topLeftRadius: CGFloat + public var topRightRadius: CGFloat + public var bottomRightRadius: CGFloat + public var bottomLeftRadius: CGFloat + public var style: ORBPath.RoundedCornerStyle + + public init(transform: CGAffineTransform, rect: CGRect, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomRightRadius: CGFloat, bottomLeftRadius: CGFloat, style: ORBPath.RoundedCornerStyle) { + self.transform = transform + self.rect = rect + self.topLeftRadius = topLeftRadius + self.topRightRadius = topRightRadius + self.bottomRightRadius = bottomRightRadius + self.bottomLeftRadius = bottomLeftRadius + self.style = style + } +} + +public struct ORBPathLineShape: @unchecked Sendable { + public var start: CGPoint + public var end: CGPoint + + public init(start: CGPoint, end: CGPoint) { + self.start = start + self.end = end + } +} + +public struct ORBPathStrokeStyle: @unchecked Sendable { + public var lineWidth: CGFloat + public var lineCap: Int32 + public var lineJoin: Int32 + public var miterLimit: CGFloat + public var dashPhase: CGFloat + public var dashCount: UInt64 + public var dash: UnsafePointer? + + public init( + lineWidth: CGFloat, + lineCap: Int32, + lineJoin: Int32, + miterLimit: CGFloat, + dashPhase: CGFloat, + dashCount: UInt64, + dash: UnsafePointer? + ) { + self.lineWidth = lineWidth + self.lineCap = lineCap + self.lineJoin = lineJoin + self.miterLimit = miterLimit + self.dashPhase = dashPhase + self.dashCount = dashCount + self.dash = dash + } +} + +public struct ORBPathStrokedShape: @unchecked Sendable { + public var path: ORBPath + public var style: ORBPathStrokeStyle + + public init(path: ORBPath, style: ORBPathStrokeStyle) { + self.path = path + self.style = style + } +} + +public struct ORBPathNestedShape: @unchecked Sendable { + public var path: ORBPath + public var transform: CGAffineTransform + + public init(path: ORBPath, transform: CGAffineTransform) { + self.path = path + self.transform = transform + } +} + +public struct ORBPathTrimmedShape: @unchecked Sendable { + public var path: ORBPath + public var from: CGFloat + public var to: CGFloat + + public init(path: ORBPath, from: CGFloat, to: CGFloat) { + self.path = path + self.from = from + self.to = to + } +} + +public struct ORBPathShape: @unchecked Sendable { + public var rect: ORBPathRectShape + public var ellipse: ORBPathRectShape + public var line: ORBPathLineShape + public var fixedRoundedRect: ORBPathFixedRoundedRectShape + public var unevenRoundedRect: ORBPathUnevenRoundedRectShape + public var nested: ORBPathNestedShape + public var trimmed: ORBPathTrimmedShape + public var stroked: ORBPathStrokedShape + + public init( + rect: ORBPathRectShape, + ellipse: ORBPathRectShape, + line: ORBPathLineShape = .init(start: .zero, end: .zero), + fixedRoundedRect: ORBPathFixedRoundedRectShape, + stroked: ORBPathStrokedShape, + unevenRoundedRect: ORBPathUnevenRoundedRectShape = .init(transform: .identity, rect: .zero, topLeftRadius: 0, topRightRadius: 0, bottomRightRadius: 0, bottomLeftRadius: 0, style: .circular) + ) { + self.rect = rect + self.ellipse = ellipse + self.line = line + self.fixedRoundedRect = fixedRoundedRect + self.nested = .init(path: stroked.path, transform: .identity) + self.trimmed = .init(path: stroked.path, from: 0, to: 1) + self.stroked = stroked + self.unevenRoundedRect = unevenRoundedRect + } +} public struct ORBPath { public var storage: ORBPath.Storage @@ -29,8 +185,24 @@ extension ORBPath { case closeSubpath = 4 case rect = 5 case roundedRect = 6 + case ellipse = 7 case fixedRoundedRectCircular = 8 case fixedRoundedRectContinuous = 9 + case unevenRoundedRectCircular = 10 + case unevenRoundedRectContinuous = 11 + case relativeArc = 12 + case arc = 13 + case arcCounterclockwise = 14 + case arcTo = 15 + case end = 16 + case transformed = 17 + case trimmed = 18 + case stroked = 19 + case boolean = 20 + case booleanSeparator = 21 + case dilateGlyph = 22 + case offset = 23 + case clipStroke = 24 case invalid = 25 } } @@ -63,95 +235,527 @@ extension ORBPath { } } -// MARK: - Static Properties +// MARK: - Portable Storage -extension ORBPath { - /// Global empty path (storage = null, callbacks = &ORBPathEmptyCallbacks) - public static var empty: ORBPath { - _openRenderBoxUnimplementedFailure() +struct _ORBPathStrokeInfo: Equatable { + var lineCap: Int32 + var lineJoin: Int32 + var dash: [CGFloat] +} + +struct _ORBPathStrokePayload { + var lineCap: Int32 + var lineJoin: Int32 + var dashCount: Int + var dash: UnsafePointer? +} + +struct _ORBPathClipRecord: Equatable { + var type: Int32 + var x: Float + var y: Float + var z: Float +} + +struct _ORBPathClipInfo: Equatable { + var flags: Int32 + var cap: Int32 + var records: [_ORBPathClipRecord] +} + +struct _ORBPathClipPayload { + var flags: Int32 + var cap: Int32 + var count: Int + var records: UnsafePointer<_ORBPathClipRecord>? +} + +struct _ORBPathRecord: Equatable { + var element: ORBPath.Element + var values: [CGFloat] + var transform: CGAffineTransform? + var stroke: _ORBPathStrokeInfo? + var boolean: [UInt8]? + var clipStroke: _ORBPathClipInfo? + + init(element: ORBPath.Element, values: [CGFloat], transform: CGAffineTransform? = nil) { + self.element = element + self.values = values + self.transform = transform?.isIdentity == false ? transform : nil } - - /// Global null path (storage = 0x1, callbacks = &ORBPathEmptyCallbacks) - public static var null: ORBPath { - _openRenderBoxUnimplementedFailure() + + init?(element: ORBPath.Element, points: UnsafePointer, userInfo: UnsafeRawPointer?) { + let counts = [2, 2, 4, 6, 0, 4, 4, 4, 6, 6, 8, 8, 5, 5, 5, 5, 0, 0, 2, 3, 0, 0, 2, 1, 1] + let index = Int(element.rawValue) + guard counts.indices.contains(index) else { return nil } + self.init( + element: element, + values: Array(UnsafeBufferPointer(start: points, count: counts[index])), + transform: ((0...15).contains(index) && index != 4) || index == 17 || index == 21 ? userInfo?.load(as: CGAffineTransform.self) : nil + ) + if element == .stroked { + let payload = userInfo?.load(as: _ORBPathStrokePayload.self) + stroke = .init(lineCap: payload?.lineCap ?? 0, lineJoin: payload?.lineJoin ?? 0, + dash: payload?.dash.map { Array(UnsafeBufferPointer(start: $0, count: max(0, payload!.dashCount))) } ?? []) + } + if element == .boolean { + boolean = userInfo.map { Array(UnsafeRawBufferPointer(start: $0, count: 2)) } ?? [0, 0] + } + if element == .clipStroke, let userInfo { + let payload = userInfo.load(as: _ORBPathClipPayload.self) + clipStroke = .init(flags: payload.flags, cap: payload.cap, + records: payload.records.map { Array(UnsafeBufferPointer(start: $0, count: max(0, payload.count))) } ?? []) + } + } + + func apply(info: UnsafeMutableRawPointer, callback: ORBPathApplyCallback) -> Bool { + // The non-null callback pointer is valid even for zero-argument elements. + var scalars = values.isEmpty ? [CGFloat(0)] : values + return scalars.withUnsafeMutableBufferPointer { points in + if let boolean { + return boolean.withUnsafeBufferPointer { callback(info, element, points.baseAddress!, $0.baseAddress) } + } + if let clipStroke { + return clipStroke.records.withUnsafeBufferPointer { records in + var payload = _ORBPathClipPayload(flags: clipStroke.flags, cap: clipStroke.cap, count: records.count, records: records.baseAddress) + return withUnsafePointer(to: &payload) { callback(info, element, points.baseAddress!, UnsafeRawPointer($0)) } + } + } + if let stroke { + let values = stroke.dash.isEmpty ? [CGFloat(0)] : stroke.dash + return values.withUnsafeBufferPointer { dash in + var payload = _ORBPathStrokePayload(lineCap: stroke.lineCap, lineJoin: stroke.lineJoin, dashCount: stroke.dash.count, dash: dash.baseAddress) + return withUnsafePointer(to: &payload) { + callback(info, element, points.baseAddress!, UnsafeRawPointer($0)) + } + } + } + if var transform { + return withUnsafePointer(to: &transform) { + callback(info, element, points.baseAddress!, UnsafeRawPointer($0)) + } + } + return callback(info, element, points.baseAddress!, nil) + } + } + + static func == (lhs: Self, rhs: Self) -> Bool { + guard lhs.element == rhs.element, lhs.values == rhs.values, lhs.transform == rhs.transform, + lhs.stroke == rhs.stroke, lhs.boolean == rhs.boolean else { return false } + let defaultClip = _ORBPathClipInfo(flags: 0, cap: 1, records: []) + return (lhs.clipStroke ?? defaultClip) == (rhs.clipStroke ?? defaultClip) + } + + var order: Int { + switch element { + case .stroked: + return stroke?.lineJoin == 1 || stroke?.lineCap == 1 ? 3 : 1 + case .clipStroke: + return clipStroke?.cap == 1 ? 3 : 1 + case .offset: return 3 + case .addQuadCurveToPoint: return 2 + case .addCurveToPoint, .ellipse, .fixedRoundedRectCircular, + .fixedRoundedRectContinuous, .unevenRoundedRectCircular, + .unevenRoundedRectContinuous, .relativeArc, .arc, + .arcCounterclockwise, .arcTo: return 3 + default: return 1 + } } } -// MARK: - Memory Management +final class _ORBPathRecords { + var records: [_ORBPathRecord] -extension ORBPath { - public func retain() { - _openRenderBoxUnimplementedFailure() + init(_ records: [_ORBPathRecord] = []) { + self.records = records } - - public func release() { - _openRenderBoxUnimplementedFailure() + + func apply(info: UnsafeMutableRawPointer, callback: ORBPathApplyCallback?) -> Bool { + guard let callback else { return true } + for record in records { + if !record.apply(info: info, callback: callback) { return false } + } + return true } } -// MARK: - Initializers +func _orbCallbackPointer(_ value: ORBPath.Callbacks) -> UnsafePointer { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.initialize(to: value) + return UnsafePointer(pointer) +} + +private let _orbEmptyCallbacks = _orbCallbackPointer(ORBPath.Callbacks()) +private let _orbRecordCallbacks: UnsafePointer = { + var callbacks = ORBPath.Callbacks() + callbacks.retain = { object in + _ = Unmanaged<_ORBPathRecords>.fromOpaque(object).retain() + return object + } + callbacks.release = { Unmanaged<_ORBPathRecords>.fromOpaque($0).release() } + callbacks.apply = { object, info, callback in + Unmanaged<_ORBPathRecords>.fromOpaque(object).takeUnretainedValue().apply(info: info, callback: callback) + } + callbacks.isEqual = { lhs, rhs in + Unmanaged<_ORBPathRecords>.fromOpaque(lhs).takeUnretainedValue().records + == Unmanaged<_ORBPathRecords>.fromOpaque(rhs).takeUnretainedValue().records + } + callbacks.isEmpty = { Unmanaged<_ORBPathRecords>.fromOpaque($0).takeUnretainedValue().records.isEmpty } + callbacks.isSingleElement = { _orbIsSingleElement(Unmanaged<_ORBPathRecords>.fromOpaque($0).takeUnretainedValue().records) } + callbacks.bezierOrder = { Unmanaged<_ORBPathRecords>.fromOpaque($0).takeUnretainedValue().records.map(\.order).max() ?? 1 } + return _orbCallbackPointer(callbacks) +}() + +let _orbCGPathCallbacks: UnsafePointer = { + var callbacks = ORBPath.Callbacks() + callbacks.retain = { object in + _ = Unmanaged.fromOpaque(object).retain() + return object + } + callbacks.release = { Unmanaged.fromOpaque($0).release() } + callbacks.apply = { object, info, callback in + let path = Unmanaged.fromOpaque(object).takeUnretainedValue() + return _ORBPathRecords(_orbRecords(path)).apply(info: info, callback: callback) + } + callbacks.isEqual = { lhs, rhs in + Unmanaged.fromOpaque(lhs).takeUnretainedValue() == Unmanaged.fromOpaque(rhs).takeUnretainedValue() + } + callbacks.isEmpty = { Unmanaged.fromOpaque($0).takeUnretainedValue().isEmpty } + callbacks.bezierOrder = { _orbRecords(Unmanaged.fromOpaque($0).takeUnretainedValue()).map(\.order).max() ?? 1 } + callbacks.cgPath = { Unmanaged.fromOpaque($0) } + return _orbCallbackPointer(callbacks) +}() extension ORBPath { - public init(cgPath: CGPath) { - _openRenderBoxUnimplementedFailure() + static func _fromRecords(_ records: [_ORBPathRecord]) -> ORBPath { + let object = Unmanaged.passRetained(_ORBPathRecords(records)).toOpaque() + return ORBPath(storage: Storage(OpaquePointer(object)), callbacks: _orbRecordCallbacks) + } + + var _object: UnsafeRawPointer { UnsafeRawPointer(storage.rawValue) } + + var _extended: UnsafePointer { + UnsafeRawPointer(callbacks).assumingMemoryBound(to: CallbacksExtended.self) + } + + func _records() -> [_ORBPathRecord]? { + var records: [_ORBPathRecord] = [] + let success = withUnsafeMutablePointer(to: &records) { records in + apply(info: UnsafeMutableRawPointer(records)) { info, element, points, userInfo in + guard let record = _ORBPathRecord(element: element, points: points, userInfo: userInfo) else { return false } + info.assumingMemoryBound(to: [_ORBPathRecord].self).pointee.append(record) + return true + } + } + return success ? records : nil + } + + public static var empty: ORBPath { + ORBPath(storage: Storage(unsafeBitCast(UInt(0), to: OpaquePointer.self)), callbacks: _orbEmptyCallbacks) + } + + public static var null: ORBPath { + ORBPath(storage: Storage(OpaquePointer(bitPattern: 1)!), callbacks: _orbEmptyCallbacks) + } + + @discardableResult + public func retain() -> ORBPath { + guard let retain = callbacks.pointee.retain else { return self } + return ORBPath(storage: Storage(OpaquePointer(retain(_object))), callbacks: callbacks) + } + + public func release() { + callbacks.pointee.release?(_object) + } + + public init(cgPath: CGPath?) { + guard let cgPath else { self = .null; return } + self.init(storage: Storage(OpaquePointer(Unmanaged.passRetained(cgPath).toOpaque())), callbacks: _orbCGPathCallbacks) } public init(rect: CGRect, transform: UnsafePointer?) { - _openRenderBoxUnimplementedFailure() + self = rect.isNull ? .empty : Self._fromRecords([_ORBPathRecord(element: .rect, values: _orbRectValues(rect), transform: transform?.pointee)]) } public init(ellipseIn rect: CGRect, transform: UnsafePointer?) { - _openRenderBoxUnimplementedFailure() + self = rect.isNull ? .empty : Self._fromRecords([_ORBPathRecord(element: .ellipse, values: _orbRectValues(rect), transform: transform?.pointee)]) } - public init(roundedRect rect: CGRect, cornerWidth: CGFloat, cornerHeight: CGFloat, style: ORBPath.RoundedCornerStyle, transform: UnsafePointer?) { - _openRenderBoxUnimplementedFailure() + public init(roundedRect rect: CGRect, cornerWidth: CGFloat, cornerHeight: CGFloat, style: RoundedCornerStyle, transform: UnsafePointer?) { + guard !rect.isNull else { self = .empty; return } + let zero = cornerWidth == 0 && cornerHeight == 0 + self = Self._fromRecords([_ORBPathRecord( + element: zero ? .roundedRect : (style == .circular ? .fixedRoundedRectCircular : .fixedRoundedRectContinuous), + values: _orbRectValues(rect) + (zero ? [] : [cornerWidth, cornerHeight]), + transform: transform?.pointee + )]) } - public init(roundedRect rect: CGRect, topLeftRadius: CGFloat, bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat, topRightRadius: CGFloat, style: ORBPath.RoundedCornerStyle, transform: UnsafePointer?) { - _openRenderBoxUnimplementedFailure() + public init(roundedRect rect: CGRect, topLeftRadius: CGFloat, topRightRadius: CGFloat, bottomRightRadius: CGFloat, bottomLeftRadius: CGFloat, style: RoundedCornerStyle, transform: UnsafePointer?) { + guard !rect.isNull else { self = .empty; return } + let radii = [topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius] + let zero = radii.allSatisfy { $0 == 0 } + self = Self._fromRecords([_ORBPathRecord( + element: zero ? .roundedRect : (style == .circular ? .unevenRoundedRectCircular : .unevenRoundedRectContinuous), + values: _orbRectValues(rect) + (zero ? [] : radii), + transform: transform?.pointee + )]) } -} - -// MARK: - Properties -extension ORBPath { public var isNull: Bool { storage == Self.null.storage && callbacks == Self.null.callbacks } - public func copy(transform: UnsafePointer) -> ORBPath { - _openRenderBoxUnimplementedFailure() + public var isEmpty: Bool { + if callbacks.pointee.flags.isExtended { + if let isEmpty = _extended.pointee.isEmpty { return isEmpty(_object, _extended) } + } else if let isEmpty = callbacks.pointee.isEmpty { + return isEmpty(_object) + } + var empty = true + _ = apply(info: &empty) { info, _, _, _ in + info.assumingMemoryBound(to: Bool.self).pointee = false + return false + } + return empty } - public var isEmpty: Bool { - _openRenderBoxUnimplementedFailure() + public var isSingleElement: Bool { + if callbacks.pointee.flags.isExtended { + if let isSingleElement = _extended.pointee.isSingleElement { return isSingleElement(_object, _extended) } + } else if let isSingleElement = callbacks.pointee.isSingleElement { + return isSingleElement(_object) + } + return _records().map(_orbIsSingleElement) ?? false } - + + public var bezierOrder: Int { + if callbacks.pointee.flags.isExtended { + if let order = _extended.pointee.bezierOrder { return order(_object, _extended) } + } else if let order = callbacks.pointee.bezierOrder { + return order(_object) + } + return _records()?.map(\.order).max() ?? 1 + } + public var cgPath: CGPath { - _openRenderBoxUnimplementedFailure() + if callbacks.pointee.flags.isExtended { + if let path = _extended.pointee.cgPath?(_object, _extended) { return path.takeUnretainedValue() } + } else if let path = callbacks.pointee.cgPath?(_object) { + return path.takeUnretainedValue() + } + guard let records = _records() else { preconditionFailure("Unsupported path element") } + return _orbMakeCGPath(records) } -} -// MARK: - Methods + public var boundingRect: CGRect { + if callbacks.pointee.flags.isExtended { + if let bounds = _extended.pointee.boundingRect { return bounds(_object, _extended) } + } else if let bounds = callbacks.pointee.boundingRect { + return bounds(_object) + } + guard let records = _records() else { return .null } + if records.count == 1, let record = records.first, (5...11).contains(record.element.rawValue) { + return _orbRect(record.values).applying(record.transform ?? .identity) + } + if _orbIsSingleElement(records), let record = records.first, record.element == .transformed { + let child = Self._fromRecords(Array(records.dropFirst().dropLast())) + defer { child.release() } + return child.boundingRect.applying(record.transform ?? .identity) + } + return _orbBounds(_orbExpandedRecords(records)) + } -extension ORBPath { public func apply(info: UnsafeMutableRawPointer, callback: ORBPathApplyCallback?) -> Bool { - _openRenderBoxUnimplementedFailure() + if callbacks.pointee.flags.isExtended { + return _extended.pointee.apply?(_object, info, callback, _extended) ?? true + } + return callbacks.pointee.apply?(_object, info, callback) ?? true } - + public func isEqual(to rhs: ORBPath) -> Bool { - _openRenderBoxUnimplementedFailure() + if callbacks != rhs.callbacks { + if callbacks == _orbEmptyCallbacks { return storage == Self.null.storage ? false : rhs.isEmpty } + if rhs.callbacks == _orbEmptyCallbacks { return rhs.storage == Self.null.storage ? false : isEmpty } + } + if callbacks == rhs.callbacks { + if storage == rhs.storage { return true } + if callbacks.pointee.flags.isExtended { + if let equal = _extended.pointee.isEqual { return equal(_object, rhs._object, _extended) } + } else if let equal = callbacks.pointee.isEqual { + return equal(_object, rhs._object) + } + } + guard let lhsRecords = _records(), let rhsRecords = rhs._records() else { return false } + return lhsRecords == rhsRecords } - + public func contains(point: CGPoint, eoFill: Bool) -> Bool { - _openRenderBoxUnimplementedFailure() + var point = point + return containsPoints(count: 1, points: &point, eoFill: eoFill, transform: nil) & 1 != 0 } - - public func containsPoints(count: UInt64, points: UnsafePointer, eoFill: Bool, transform: UnsafePointer?) -> Bool { - _openRenderBoxUnimplementedFailure() + + public func containsPoints(count: UInt64, points: UnsafePointer, eoFill: Bool, transform: UnsafePointer?) -> UInt64 { + guard count > 0, let records = _records() else { return 0 } + let queries = (0.. Bool { + guard let records = _records() else { return false } + let zeroRect = ORBPathRectShape(transform: .identity, rect: .zero) + var shape = ORBPathShape( + rect: zeroRect, ellipse: zeroRect, + fixedRoundedRect: .init(transform: .identity, rect: .zero, cornerSize: .zero, style: .circular), + stroked: .init(path: self, style: .init(lineWidth: 0, lineCap: 0, lineJoin: 0, miterLimit: 0, dashPhase: 0, dashCount: 0, dash: nil)) + ) + let type: ORBPathShapeType + var ownedChild: ORBPath? + var strokeDash: [CGFloat]? + defer { ownedChild?.release() } + if let record = records.first, records.last?.element == .end, + [.transformed, .trimmed, .stroked].contains(record.element), _orbIsSingleElement(records) { + let child: ORBPath + if callbacks == _orbWrapperCallbacks { + child = Unmanaged<_ORBPathWrapper>.fromOpaque(_object).takeUnretainedValue().child + } else { + child = Self._fromRecords(Array(records.dropFirst().dropLast())) + ownedChild = child + } + switch record.element { + case .transformed: + type = .nested + shape.nested = .init(path: child, transform: record.transform ?? .identity) + case .trimmed: + type = .trimmed + shape.trimmed = .init(path: child, from: record.values[0], to: record.values[1]) + default: + type = .stroked + strokeDash = record.stroke?.dash ?? [] + shape.stroked = .init(path: child, style: .init(lineWidth: record.values[0], lineCap: record.stroke?.lineCap ?? 0, + lineJoin: record.stroke?.lineJoin ?? 0, miterLimit: record.values[1], dashPhase: record.values[2], + dashCount: UInt64(strokeDash!.count), dash: nil)) + } + } else if records.count == 1, let record = records.first { + switch record.element { + case .rect, .roundedRect, .ellipse, .fixedRoundedRectCircular, .fixedRoundedRectContinuous, .unevenRoundedRectCircular, .unevenRoundedRectContinuous: + let rect = _orbRect(record.values) + let transform = record.transform ?? .identity + shape.rect = .init(transform: transform, rect: rect) + shape.ellipse = shape.rect + switch record.element { + case .rect, .roundedRect: type = .rect + case .ellipse: type = .ellipse + case .fixedRoundedRectCircular, .fixedRoundedRectContinuous: + type = .fixedRoundedRect + shape.fixedRoundedRect = .init(transform: transform, rect: rect, cornerSize: CGSize(width: record.values[4], height: record.values[5]), style: record.element == .fixedRoundedRectContinuous ? .continuous : .circular) + default: + type = .unevenRoundedRect + shape.unevenRoundedRect = .init(transform: transform, rect: rect, topLeftRadius: record.values[4], topRightRadius: record.values[5], bottomRightRadius: record.values[6], bottomLeftRadius: record.values[7], style: record.element == .unevenRoundedRectContinuous ? .continuous : .circular) + } + default: return false + } + } else if !allowLine, records.count == 2, records[0].element == .moveToPoint, records[1].element == .addLineToPoint { + type = .line + shape.line = .init(start: _orbPoint(records[0].values, 0), end: _orbPoint(records[1].values, 0)) + } else if let rect = _orbRecognizeRect(records) { + type = .rect + shape.rect = .init(transform: .identity, rect: rect) + } else { + return false + } + if let strokeDash { + let values = strokeDash.isEmpty ? [CGFloat(0)] : strokeDash + values.withUnsafeBufferPointer { dash in + shape.stroked.style.dash = dash.baseAddress + withUnsafePointer(to: &shape) { callback(info, type, $0) } + } + } else { + withUnsafePointer(to: &shape) { callback(info, type, $0) } + } + return true } } -#endif +func _orbIsSingleElement(_ records: [_ORBPathRecord]) -> Bool { + var depth = 0 + var count = 0 + for record in records { + switch record.element { + case .end: + if depth == 0 { return false } + depth -= 1 + case .transformed, .trimmed, .stroked, .boolean, .dilateGlyph, .offset, .clipStroke: + if depth == 0 { count += 1 } + depth += 1 + default: + if depth == 0 { count += 1 } + } + if count > 1 { return false } + } + return count == 1 && depth == 0 +} +private final class _ORBPathWrapper { + let child: ORBPath + let record: _ORBPathRecord + + init(child: ORBPath, record: _ORBPathRecord) { + self.child = child.retain() + self.record = record + } + + deinit { child.release() } +} + +private let _orbWrapperCallbacks: UnsafePointer = { + var callbacks = ORBPath.Callbacks() + callbacks.retain = { object in + _ = Unmanaged<_ORBPathWrapper>.fromOpaque(object).retain() + return object + } + callbacks.release = { Unmanaged<_ORBPathWrapper>.fromOpaque($0).release() } + callbacks.apply = { object, info, callback in + guard let callback else { return true } + let wrapper = Unmanaged<_ORBPathWrapper>.fromOpaque(object).takeUnretainedValue() + guard wrapper.record.apply(info: info, callback: callback), + wrapper.child.apply(info: info, callback: callback) else { return false } + return _ORBPathRecord(element: .end, values: []).apply(info: info, callback: callback) + } + callbacks.isEqual = { lhs, rhs in + let a = Unmanaged<_ORBPathWrapper>.fromOpaque(lhs).takeUnretainedValue() + let b = Unmanaged<_ORBPathWrapper>.fromOpaque(rhs).takeUnretainedValue() + return a.record == b.record && a.child.isEqual(to: b.child) + } + callbacks.isEmpty = { _ in false } + callbacks.isSingleElement = { _ in true } + callbacks.next = { Unmanaged<_ORBPathWrapper>.fromOpaque($0).takeUnretainedValue().child } + return _orbCallbackPointer(callbacks) +}() + +extension ORBPath { + private func _wrapped(_ record: _ORBPathRecord) -> ORBPath { + let object = Unmanaged.passRetained(_ORBPathWrapper(child: self, record: record)).toOpaque() + return ORBPath(storage: Storage(OpaquePointer(object)), callbacks: _orbWrapperCallbacks) + } + + public func copy(transform: UnsafePointer?) -> ORBPath { + if callbacks == _orbEmptyCallbacks { return self } + return _wrapped(.init(element: .transformed, values: [], transform: transform?.pointee)) + } + + public func trimmed(from: CGFloat, to: CGFloat) -> ORBPath { + if callbacks == _orbEmptyCallbacks { return .empty } + return _wrapped(.init(element: .trimmed, values: [from, to])) + } + + public func stroked(lineWidth: CGFloat, lineCap: Int32, lineJoin: Int32, miterLimit: CGFloat, dashPhase: CGFloat, dash: UnsafePointer?, dashCount: Int) -> ORBPath { + if callbacks == _orbEmptyCallbacks { return .empty } + var record = _ORBPathRecord(element: .stroked, values: [lineWidth, miterLimit, dashPhase]) + record.stroke = .init(lineCap: lineCap, lineJoin: lineJoin, dash: dash.map { Array(UnsafeBufferPointer(start: $0, count: max(0, dashCount))) } ?? []) + return _wrapped(record) + } +} + + +#endif diff --git a/Sources/OpenRenderBox/Path/ORBPathCallbacks.swift b/Sources/OpenRenderBox/Path/ORBPathCallbacks.swift index 4b10fff..aa3c7ea 100644 --- a/Sources/OpenRenderBox/Path/ORBPathCallbacks.swift +++ b/Sources/OpenRenderBox/Path/ORBPathCallbacks.swift @@ -44,10 +44,10 @@ extension ORBPath { public var isEqual: ((UnsafeRawPointer, UnsafeRawPointer) -> Bool)? public var isEmpty: ((UnsafeRawPointer) -> Bool)? public var isSingleElement: ((UnsafeRawPointer) -> Bool)? - public var bezierOrder: ((UnsafeRawPointer) -> UInt32)? + public var bezierOrder: ((UnsafeRawPointer) -> Int)? public var boundingRect: ((UnsafeRawPointer) -> CGRect)? public var cgPath: ((UnsafeRawPointer) -> Unmanaged?)? - public var next: ((UnsafeRawPointer) -> UnsafePointer?)? + public var next: ((UnsafeRawPointer) -> ORBPath)? public init() { self.flags = ORBPath.CallbacksFlags() @@ -71,10 +71,10 @@ extension ORBPath { isEqual: ((UnsafeRawPointer, UnsafeRawPointer) -> Bool)?, isEmpty: ((UnsafeRawPointer) -> Bool)?, isSingleElement: ((UnsafeRawPointer) -> Bool)?, - bezierOrder: ((UnsafeRawPointer) -> UInt32)?, + bezierOrder: ((UnsafeRawPointer) -> Int)?, boundingRect: ((UnsafeRawPointer) -> CGRect)?, cgPath: ((UnsafeRawPointer) -> Unmanaged?)?, - next: ((UnsafeRawPointer) -> UnsafePointer?)? + next: ((UnsafeRawPointer) -> ORBPath)? ) { self.flags = flags self.retain = retain @@ -96,7 +96,7 @@ extension ORBPath { extension ORBPath.Callbacks { /// Global callbacks for CGPath-backed paths public static var cgPath: ORBPath.Callbacks { - _openRenderBoxUnimplementedFailure() + _orbCGPathCallbacks.pointee } } @@ -112,10 +112,10 @@ extension ORBPath { public var isEqual: ((UnsafeRawPointer, UnsafeRawPointer, UnsafePointer) -> Bool)? public var isEmpty: ((UnsafeRawPointer, UnsafePointer) -> Bool)? public var isSingleElement: ((UnsafeRawPointer, UnsafePointer) -> Bool)? - public var bezierOrder: ((UnsafeRawPointer, UnsafePointer) -> UInt32)? + public var bezierOrder: ((UnsafeRawPointer, UnsafePointer) -> Int)? public var boundingRect: ((UnsafeRawPointer, UnsafePointer) -> CGRect)? public var cgPath: ((UnsafeRawPointer, UnsafePointer) -> Unmanaged?)? - public var next: ((UnsafeRawPointer, UnsafePointer) -> UnsafePointer?)? + public var next: ((UnsafeRawPointer, UnsafePointer) -> ORBPath)? public init() { self.flags = ORBPath.CallbacksFlags() @@ -139,10 +139,10 @@ extension ORBPath { isEqual: ((UnsafeRawPointer, UnsafeRawPointer, UnsafePointer) -> Bool)?, isEmpty: ((UnsafeRawPointer, UnsafePointer) -> Bool)?, isSingleElement: ((UnsafeRawPointer, UnsafePointer) -> Bool)?, - bezierOrder: ((UnsafeRawPointer, UnsafePointer) -> UInt32)?, + bezierOrder: ((UnsafeRawPointer, UnsafePointer) -> Int)?, boundingRect: ((UnsafeRawPointer, UnsafePointer) -> CGRect)?, cgPath: ((UnsafeRawPointer, UnsafePointer) -> Unmanaged?)?, - next: ((UnsafeRawPointer, UnsafePointer) -> UnsafePointer?)? + next: ((UnsafeRawPointer, UnsafePointer) -> ORBPath)? ) { self.flags = flags self.retain = retain diff --git a/Sources/OpenRenderBox/Path/ORBPathGeometry.swift b/Sources/OpenRenderBox/Path/ORBPathGeometry.swift new file mode 100644 index 0000000..bb149dc --- /dev/null +++ b/Sources/OpenRenderBox/Path/ORBPathGeometry.swift @@ -0,0 +1,728 @@ +// +// ORBPathGeometry.swift +// OpenRenderBox + +#if !OPENRENDERBOX_CF_CGTYPES +import Foundation +import OpenCoreGraphicsShims + +func _orbRectValues(_ rect: CGRect) -> [CGFloat] { + [rect.origin.x, rect.origin.y, rect.size.width, rect.size.height] +} + +func _orbRect(_ values: [CGFloat]) -> CGRect { + CGRect(x: values[0], y: values[1], width: values[2], height: values[3]) +} + +func _orbPoint(_ values: [CGFloat], _ index: Int) -> CGPoint { + CGPoint(x: values[index * 2], y: values[index * 2 + 1]) +} + +func _orbRecords(_ path: CGPath) -> [_ORBPathRecord] { + var records: [_ORBPathRecord] = [] + #if OPENRENDERBOX_COREGRAPHICS + path.applyWithBlock { pointer in + let value = pointer.pointee + let element: ORBPath.Element + let count: Int + switch value.type { + case .moveToPoint: element = .moveToPoint; count = 1 + case .addLineToPoint: element = .addLineToPoint; count = 1 + case .addQuadCurveToPoint: element = .addQuadCurveToPoint; count = 2 + case .addCurveToPoint: element = .addCurveToPoint; count = 3 + case .closeSubpath: element = .closeSubpath; count = 0 + @unknown default: return + } + records.append(_ORBPathRecord(element: element, values: (0.. CGPath { + let path = CGMutablePath() + for record in _orbExpandedRecords(records) { + switch record.element { + case .moveToPoint: path.move(to: _orbPoint(record.values, 0)) + case .addLineToPoint: path.addLine(to: _orbPoint(record.values, 0)) + case .addQuadCurveToPoint: path.addQuadCurve(to: _orbPoint(record.values, 1), control: _orbPoint(record.values, 0)) + case .addCurveToPoint: path.addCurve(to: _orbPoint(record.values, 2), control1: _orbPoint(record.values, 0), control2: _orbPoint(record.values, 1)) + case .closeSubpath: path.closeSubpath() + default: preconditionFailure("Unsupported path element") + } + } + return path +} + +struct _ORBBezierSegment { + var points: [CGPoint] + + func point(_ t: CGFloat) -> CGPoint { + var values = points + for level in stride(from: values.count - 1, through: 1, by: -1) { + for index in 0..(_ keyPath: KeyPath) -> [CGFloat] { + let p = points.map { CGFloat($0[keyPath: keyPath]) } + let roots: [CGFloat] + switch p.count { + case 3: + let a = p[0] - 2 * p[1] + p[2] + roots = a == 0 ? [] : [(p[0] - p[1]) / a] + case 4: + roots = _orbQuadraticRoots(-p[0] + 3 * p[1] - 3 * p[2] + p[3], 2 * (p[0] - 2 * p[1] + p[2]), p[1] - p[0]) + default: roots = [] + } + return roots.filter { $0 > 0 && $0 < 1 }.sorted() + } +} + +func _orbQuadraticRoots(_ a: CGFloat, _ b: CGFloat, _ c: CGFloat) -> [CGFloat] { + if a == 0 { return b == 0 ? [] : [-c / b] } + let discriminant = b * b - 4 * a * c + if discriminant < 0 { return [] } + if discriminant == 0 { return [-b / (2 * a)] } + let root = discriminant.squareRoot() + let q = -(b + (b >= 0 ? root : -root)) / 2 + return [q / a, c / q] +} + +func _orbSegments(_ records: [_ORBPathRecord], closeOpenSubpaths: Bool) -> [_ORBBezierSegment] { + var segments: [_ORBBezierSegment] = [] + var current: CGPoint? + var start: CGPoint? + func close() { + if let current, let start, current != start { + segments.append(.init(points: [current, start])) + } + } + for record in records { + switch record.element { + case .moveToPoint: + if closeOpenSubpaths { close() } + current = _orbPoint(record.values, 0) + start = current + case .addLineToPoint, .addQuadCurveToPoint, .addCurveToPoint: + let points = stride(from: 0, to: record.values.count, by: 2).map { CGPoint(x: record.values[$0], y: record.values[$0 + 1]) } + if let current { + segments.append(.init(points: [current] + points)) + } else { + start = points.last + } + current = points.last + case .closeSubpath: + close() + current = start + default: break + } + } + if closeOpenSubpaths { close() } + return segments +} + +func _orbBounds(_ records: [_ORBPathRecord]) -> CGRect { + var bounds = CGRect.null + func include(_ point: CGPoint) { + guard point.x.isFinite && point.y.isFinite else { return } + let rect = CGRect(origin: point, size: .zero) + bounds = bounds.isNull ? rect : bounds.union(rect) + } + for record in records where record.element == .moveToPoint { + include(_orbPoint(record.values, 0)) + } + for segment in _orbSegments(records, closeOpenSubpaths: false) { + include(segment.points[0]) + include(segment.points.last!) + for t in segment.extrema(\.x) + segment.extrema(\.y) { include(segment.point(t)) } + } + return bounds +} + +func _orbRecognizeRect(_ records: [_ORBPathRecord]) -> CGRect? { + guard records.count == 5, records[0].element == .moveToPoint, + records[1...3].allSatisfy({ $0.element == .addLineToPoint }), + records[4].element == .closeSubpath else { return nil } + let points = records.prefix(4).map { _orbPoint($0.values, 0) } + let bounds = _orbBounds(records) + guard !bounds.isNull else { return nil } + let corners = [CGPoint(x: bounds.minX, y: bounds.minY), CGPoint(x: bounds.maxX, y: bounds.minY), CGPoint(x: bounds.maxX, y: bounds.maxY), CGPoint(x: bounds.minX, y: bounds.maxY)] + guard points.allSatisfy({ corners.contains($0) }) else { return nil } + for index in 0..<4 { + let a = points[index] + let b = points[(index + 1) % 4] + if a == b || (a.x != b.x && a.y != b.y) { return nil } + } + return bounds +} + +func _orbClampRadii(_ input: [CGFloat], width: CGFloat, height: CGFloat) -> [CGFloat] { + let width = abs(width) + let height = abs(height) + let limit = max(width, height) + var radii = input.map { min(max($0, 0), limit) } + for (a, b, dimension) in [(0, 1, width), (3, 2, width), (0, 3, height), (1, 2, height)] { + var deficit = dimension - (radii[a] + radii[b]) + if deficit < 0, radii[a] != radii[b] { + let reduction = min(-deficit, abs(radii[a] - radii[b])) + if radii[a] > radii[b] { radii[a] -= reduction } else { radii[b] -= reduction } + deficit += reduction + } + if deficit < 0 { + let change = radii[a] * deficit / (radii[a] + radii[b]) + radii[a] += change + radii[b] += deficit - change + } + } + return radii +} + +func _orbExpandedRecords(_ records: [_ORBPathRecord]) -> [_ORBPathRecord] { + var result: [_ORBPathRecord] = [] + var frames: [(_ORBPathRecord, [_ORBPathRecord])] = [] + for record in records { + switch record.element { + case .transformed, .trimmed, .stroked: + frames.append((record, result)) + result = [] + continue + case .end: + if let (header, parent) = frames.popLast() { + result = parent + _orbFinishFrame(header, result) + } + continue + default: break + } + let transform = record.transform ?? .identity + var expanded: [_ORBPathRecord] + switch record.element { + case .moveToPoint, .addLineToPoint, .addQuadCurveToPoint, .addCurveToPoint, .closeSubpath: + expanded = [record] + case .rect: + let rect = _orbRect(record.values) + expanded = [ + .init(element: .moveToPoint, values: [rect.minX, rect.minY]), + .init(element: .addLineToPoint, values: [rect.maxX, rect.minY]), + .init(element: .addLineToPoint, values: [rect.maxX, rect.maxY]), + .init(element: .addLineToPoint, values: [rect.minX, rect.maxY]), + .init(element: .closeSubpath, values: []) + ] + case .ellipse: + let rect = _orbRect(record.values) + let rx = rect.width / 2 + let ry = rect.height / 2 + let cx = rect.midX + let cy = rect.midY + let k = CGFloat(0.5522847498) + expanded = [ + .init(element: .moveToPoint, values: [cx + rx, cy]), + .init(element: .addCurveToPoint, values: [cx + rx, cy + ry * k, cx + rx * k, cy + ry, cx, cy + ry]), + .init(element: .addCurveToPoint, values: [cx - rx * k, cy + ry, cx - rx, cy + ry * k, cx - rx, cy]), + .init(element: .addCurveToPoint, values: [cx - rx, cy - ry * k, cx - rx * k, cy - ry, cx, cy - ry]), + .init(element: .addCurveToPoint, values: [cx + rx * k, cy - ry, cx + rx, cy - ry * k, cx + rx, cy]), + .init(element: .closeSubpath, values: []) + ] + case .roundedRect, .fixedRoundedRectCircular, .fixedRoundedRectContinuous, .unevenRoundedRectCircular, .unevenRoundedRectContinuous: + expanded = _orbRoundedRecords(record) + case .relativeArc, .arc, .arcCounterclockwise, .arcTo: + let current = _orbCurrentPoint(result).map { $0.applying(transform.inverted()) } + expanded = _orbArcRecords(record, current: current) + default: + preconditionFailure("Unsupported path element") + } + if !transform.isIdentity { + expanded = expanded.map { entry in + var entry = entry + for index in stride(from: 0, to: entry.values.count, by: 2) { + let point = CGPoint(x: entry.values[index], y: entry.values[index + 1]).applying(transform) + entry.values[index] = point.x + entry.values[index + 1] = point.y + } + entry.transform = nil + return entry + } + } + result.append(contentsOf: expanded) + } + while let (header, parent) = frames.popLast() { + result = parent + _orbFinishFrame(header, result) + } + return result +} + +func _orbRoundedRecords(_ record: _ORBPathRecord) -> [_ORBPathRecord] { + let rect = _orbRect(record.values).standardized + let continuous = record.element == .fixedRoundedRectContinuous || record.element == .unevenRoundedRectContinuous + let rx: [CGFloat] + let ry: [CGFloat] + if record.values.count == 8 { + rx = _orbClampRadii(Array(record.values[4...7]), width: rect.width, height: rect.height) + ry = rx + } else if record.values.count == 6 { + let limit = min(rect.width, rect.height) / 2 + rx = Array(repeating: min(max(record.values[4], 0), limit), count: 4) + ry = Array(repeating: min(max(record.values[5], 0), limit), count: 4) + } else { + rx = Array(repeating: 0, count: 4) + ry = rx + } + var result: [_ORBPathRecord] = [.init(element: .moveToPoint, values: [rect.maxX, (rect.minY + ry[1] + rect.maxY - ry[2]) / 2])] + if rx.allSatisfy({ $0 == 0 }) && ry.allSatisfy({ $0 == 0 }) { + result += [ + .init(element: .addLineToPoint, values: [rect.maxX, rect.maxY]), + .init(element: .addLineToPoint, values: [rect.minX, rect.maxY]), + .init(element: .addLineToPoint, values: [rect.minX, rect.minY]), + .init(element: .addLineToPoint, values: [rect.maxX, rect.minY]), + .init(element: .closeSubpath, values: []) + ] + return result + } + // The sequence begins at the right edge, then visits BR, BL, TL, and TR. + let corners = [2, 3, 0, 1] + let vertices = [CGPoint(x: rect.maxX, y: rect.maxY), CGPoint(x: rect.minX, y: rect.maxY), CGPoint(x: rect.minX, y: rect.minY), CGPoint(x: rect.maxX, y: rect.minY)] + let incoming = [CGPoint(x: 0, y: -1), CGPoint(x: 1, y: 0), CGPoint(x: 0, y: 1), CGPoint(x: -1, y: 0)] + let outgoing = [CGPoint(x: -1, y: 0), CGPoint(x: 0, y: -1), CGPoint(x: 1, y: 0), CGPoint(x: 0, y: 1)] + let k = CGFloat(0.5522847498) + for index in 0..<4 { + let corner = corners[index] + let vertex = vertices[index] + let incomingRadius = index & 1 == 0 ? ry[corner] : rx[corner] + let outgoingRadius = index & 1 == 0 ? rx[corner] : ry[corner] + func point(_ a: CGFloat, _ b: CGFloat) -> [CGFloat] { + [vertex.x + incoming[index].x * incomingRadius * a + outgoing[index].x * outgoingRadius * b, + vertex.y + incoming[index].y * incomingRadius * a + outgoing[index].y * outgoingRadius * b] + } + if continuous { + let previous = corners[(index + 3) % 4] + let next = corners[(index + 1) % 4] + let incomingDimension = index & 1 == 0 ? rect.height : rect.width + let outgoingDimension = index & 1 == 0 ? rect.width : rect.height + let previousRadius = index & 1 == 0 ? ry[previous] : rx[previous] + let nextRadius = index & 1 == 0 ? rx[next] : ry[next] + let a = _orbContinuousCoefficients(incomingDimension, incomingRadius, previousRadius) + let b = _orbContinuousCoefficients(outgoingDimension, outgoingRadius, nextRadius) + let u = CGFloat(Float(0.0749114)) + let v = CGFloat(Float(0.631494)) + let c = CGFloat(Float(0.16906)) + let d = CGFloat(Float(0.372824)) + result.append(.init(element: .addLineToPoint, values: point(a.0, 0))) + result.append(.init(element: .addCurveToPoint, values: point(a.1, 0) + point(a.2, 0) + point(v, u))) + result.append(.init(element: .addCurveToPoint, values: point(d, c) + point(c, d) + point(u, v))) + result.append(.init(element: .addCurveToPoint, values: point(0, b.2) + point(0, b.1) + point(0, b.0))) + } else { + result.append(.init(element: .addLineToPoint, values: point(1, 0))) + result.append(.init(element: .addCurveToPoint, values: point(1 - k, 0) + point(0, 1 - k) + point(0, 1))) + } + } + result.append(.init(element: .closeSubpath, values: [])) + return result +} + +func _orbContinuousCoefficients(_ length: CGFloat, _ a: CGFloat, _ b: CGFloat) -> (CGFloat, CGFloat, CGFloat) { + let sum = Float(a) + Float(b) + let delta = Float(bitPattern: 0x3f075696) + let ratio = (Float(abs(length)) - sum) / (sum * delta) + if ratio < 1 { + let t = min(max(ratio, 0), 1) + return (CGFloat(Float(1).addingProduct(t, delta)), + CGFloat(Float(0.96).addingProduct(t, Float(bitPattern: 0x3e0392e4))), + CGFloat(Float(0.82).addingProduct(t, Float(bitPattern: 0x3d464670)))) + } + return (CGFloat(Float(1.528665)), CGFloat(Float(1.08849)), CGFloat(Float(0.868407))) +} + +func _orbMix(_ a: CGPoint, _ b: CGPoint, _ t: CGFloat) -> CGPoint { + CGPoint(x: a.x.addingProduct(b.x - a.x, t), y: a.y.addingProduct(b.y - a.y, t)) +} + +func _orbDistance(_ a: CGPoint, _ b: CGPoint) -> CGFloat { + let x = b.x - a.x, y = b.y - a.y + return (x * x + y * y).squareRoot() +} + +struct _ORBCubic { + var points: [CGPoint] + + func split(_ t: CGFloat) -> (_ORBCubic, _ORBCubic) { + let a = _orbMix(points[0], points[1], t) + let b = _orbMix(points[1], points[2], t) + let c = _orbMix(points[2], points[3], t) + let d = _orbMix(a, b, t), e = _orbMix(b, c, t), f = _orbMix(d, e, t) + return (.init(points: [points[0], a, d, f]), .init(points: [f, e, c, points[3]])) + } + + func measure(_ requested: CGFloat = 0, findIndex: Bool = false) -> CGFloat { + var curve = self + var edge0 = _orbDistance(curve.points[0], curve.points[1]) + var edge2 = _orbDistance(curve.points[2], curve.points[3]) + if !(edge0 + edge2).isFinite || (findIndex && requested == 0) { return 0 } + var pending: [(_ORBCubic, CGFloat)] = [] + var interval: CGFloat = 1, parameter: CGFloat = 0, total: CGFloat = 0 + for _ in 0..<1024 { + edge0 = _orbDistance(curve.points[0], curve.points[1]) + let edge1 = _orbDistance(curve.points[1], curve.points[2]) + edge2 = _orbDistance(curve.points[2], curve.points[3]) + let polygon = edge0 + edge2 + edge1 + let chord = _orbDistance(curve.points[0], curve.points[3]) + var flat = polygon - chord <= 0.5 * interval + if findIndex { flat = flat && abs(edge0 - edge1) + abs(edge1 - edge2) < 0.5 } + if flat || pending.count == 29 { + let length = (polygon + chord) * 0.5 + if findIndex && total + length >= requested { + return min(parameter + (requested - total) * interval / length, 1) + } + total += length + guard let next = pending.popLast() else { return findIndex ? 1 : total } + parameter += interval + curve = next.0 + interval = next.1 + } else { + let (left, right) = curve.split(0.5) + interval *= 0.5 + pending.append((right, interval)) + curve = left + } + } + return 1 + } +} + +func _orbTrimmedRecords(_ records: [_ORBPathRecord], from: Float, to: Float) -> [_ORBPathRecord] { + if from <= 0 && to >= 1 { return records } + guard from < to else { return [] } + struct Segment { + var element: ORBPath.Element + var curve: _ORBCubic + var length: CGFloat + } + var segments: [Segment] = [] + var current = CGPoint.zero, start = CGPoint.zero + var total: CGFloat = 0 + for record in records { + var curve = _ORBCubic(points: [current, .zero, .zero, .zero]) + var length: CGFloat = 0 + switch record.element { + case .moveToPoint: + current = _orbPoint(record.values, 0) + start = current + curve.points[0] = current + case .addLineToPoint: + curve.points[3] = _orbPoint(record.values, 0) + length = _orbDistance(current, curve.points[3]) + current = curve.points[3] + case .addQuadCurveToPoint: + let control = _orbPoint(record.values, 0), end = _orbPoint(record.values, 1) + curve.points[1] = CGPoint(x: (current.x + 2 * control.x) / 3, y: (current.y + 2 * control.y) / 3) + curve.points[2] = CGPoint(x: (end.x + 2 * control.x) / 3, y: (end.y + 2 * control.y) / 3) + curve.points[3] = end + length = curve.measure() + current = end + case .addCurveToPoint: + curve.points = [current, _orbPoint(record.values, 0), _orbPoint(record.values, 1), _orbPoint(record.values, 2)] + length = curve.measure() + current = curve.points[3] + case .closeSubpath: + curve.points[3] = start + length = _orbDistance(current, start) + current = start + default: continue + } + total += length + segments.append(.init(element: record.element, curve: curve, length: length)) + } + let fromDistance = total * CGFloat(from), toDistance = total * CGFloat(to) + var consumed: Float = 0 + var needsMove = true, includesStart = false + var result: [_ORBPathRecord] = [] + func move(_ point: CGPoint) { + result.append(.init(element: .moveToPoint, values: [point.x, point.y])) + } + for segment in segments { + if segment.element == .moveToPoint { + needsMove = true + includesStart = false + continue + } + let begin = CGFloat(consumed), end = begin + segment.length + consumed = Float(end) + if segment.length == 0 { + if segment.element == .closeSubpath && includesStart && begin <= toDistance { + result.append(.init(element: .closeSubpath, values: [])) + } + continue + } + var first = fromDistance >= begin ? max((fromDistance - begin) / segment.length, 0) : 0 + var last = toDistance < end ? min((toDistance - begin) / segment.length, 1) : 1 + guard first < last else { continue } + if needsMove { includesStart = first == 0 } + if segment.element == .addLineToPoint || segment.element == .closeSubpath { + let start = _orbMix(segment.curve.points[0], segment.curve.points[3], first) + let finish = _orbMix(segment.curve.points[0], segment.curve.points[3], last) + if needsMove { move(start); needsMove = false } + result.append(.init(element: .addLineToPoint, values: [finish.x, finish.y])) + if segment.element == .closeSubpath && last == 1 && includesStart { + result.append(.init(element: .closeSubpath, values: [])) + } + } else { + var curve = segment.curve + if first > 0 || last < 1 { + if first > 0.001 { first = curve.measure(first * segment.length, findIndex: true) } + if last < 0.999 { last = curve.measure(last * segment.length, findIndex: true) } + if first > 0.001 { + curve = curve.split(first).1 + last = (last - first) / (1 - first) + } + if last < 0.999 { curve = curve.split(last).0 } + } + if needsMove { move(curve.points[0]); needsMove = false } + result.append(.init(element: .addCurveToPoint, values: curve.points.dropFirst().flatMap { [$0.x, $0.y] })) + } + } + return result +} + +func _orbFinishFrame(_ header: _ORBPathRecord, _ records: [_ORBPathRecord]) -> [_ORBPathRecord] { + switch header.element { + case .transformed: + let transform = header.transform ?? .identity + return records.map { record in + var record = record + for index in stride(from: 0, to: record.values.count, by: 2) { + let point = CGPoint(x: record.values[index], y: record.values[index + 1]).applying(transform) + record.values[index] = point.x + record.values[index + 1] = point.y + } + return record + } + case .trimmed: + return _orbTrimmedRecords(records, from: Float(header.values[0]), to: Float(header.values[1])) + case .stroked: + #if OPENRENDERBOX_COREGRAPHICS + var path = _orbMakeCGPath(records) + if let stroke = header.stroke, !stroke.dash.isEmpty { + path = path.copy(dashingWithPhase: CGFloat(Float(header.values[2])), lengths: stroke.dash) + } + let cap = CGLineCap(rawValue: header.stroke?.lineCap ?? 0) ?? .butt + let join = CGLineJoin(rawValue: header.stroke?.lineJoin ?? 0) ?? .miter + return _orbRecords(path.copy(strokingWithWidth: CGFloat(Float(header.values[0])), lineCap: cap, lineJoin: join, miterLimit: CGFloat(Float(header.values[1])))) + #else + preconditionFailure("Stroke conversion requires CoreGraphics") + #endif + default: + preconditionFailure("Unsupported path wrapper") + } +} + +func _orbCurrentPoint(_ records: [_ORBPathRecord]) -> CGPoint? { + var current: CGPoint? + var start: CGPoint? + for record in records { + switch record.element { + case .moveToPoint: current = _orbPoint(record.values, 0); start = current + case .addLineToPoint, .addQuadCurveToPoint, .addCurveToPoint: + current = _orbPoint(record.values, record.values.count / 2 - 1) + case .closeSubpath: current = start + default: break + } + } + return current +} + +func _orbArcRecords(_ record: _ORBPathRecord, current: CGPoint?) -> [_ORBPathRecord] { + let v = record.values + #if OPENRENDERBOX_COREGRAPHICS + let path = CGMutablePath() + if let current { path.move(to: current) } + switch record.element { + case .relativeArc: path.addRelativeArc(center: CGPoint(x: v[0], y: v[1]), radius: v[2], startAngle: v[3], delta: v[4]) + case .arc, .arcCounterclockwise: path.addArc(center: CGPoint(x: v[0], y: v[1]), radius: v[2], startAngle: v[3], endAngle: v[4], clockwise: record.element == .arc) + case .arcTo: path.addArc(tangent1End: CGPoint(x: v[0], y: v[1]), tangent2End: CGPoint(x: v[2], y: v[3]), radius: v[4]) + default: return [] + } + let records = _orbRecords(path) + return current == nil ? records : Array(records.dropFirst()) + #else + if record.element == .arcTo { + let corner = CGPoint(x: v[0], y: v[1]), endpoint = CGPoint(x: v[2], y: v[3]) + guard let current else { return [.init(element: .moveToPoint, values: [corner.x, corner.y])] } + let a = CGPoint(x: current.x - corner.x, y: current.y - corner.y) + let b = CGPoint(x: endpoint.x - corner.x, y: endpoint.y - corner.y) + let al = (a.x * a.x + a.y * a.y).squareRoot(), bl = (b.x * b.x + b.y * b.y).squareRoot() + let cross = a.x * b.y - a.y * b.x + guard al > 0, bl > 0, cross != 0, v[4] > 0 else { + return [.init(element: .addLineToPoint, values: [corner.x, corner.y])] + } + let u = CGPoint(x: a.x / al, y: a.y / al), w = CGPoint(x: b.x / bl, y: b.y / bl) + let angle = acos(min(max(u.x * w.x + u.y * w.y, -1), 1)) + let distance = v[4] / tan(angle / 2) + let first = CGPoint(x: corner.x + u.x * distance, y: corner.y + u.y * distance) + let last = CGPoint(x: corner.x + w.x * distance, y: corner.y + w.y * distance) + let sign: CGFloat = cross < 0 ? 1 : -1 + let center = CGPoint(x: first.x + sign * u.y * v[4], y: first.y - sign * u.x * v[4]) + let start = atan2(first.y - center.y, first.x - center.x) + let finish = atan2(last.y - center.y, last.x - center.x) + var delta = finish - start + if sign > 0 { while delta < 0 { delta += 2 * .pi } } else { while delta > 0 { delta -= 2 * .pi } } + return _orbCircularArc(center: center, radius: v[4], start: start, delta: delta, current: current) + } + var delta = v[4] + if record.element != .relativeArc { + delta -= v[3] + if record.element == .arc, delta > 0 { + delta -= ceil(delta / (2 * .pi)) * (2 * .pi) + } else if record.element == .arcCounterclockwise, delta < 0 { + delta += ceil(-delta / (2 * .pi)) * (2 * .pi) + } + } + return _orbCircularArc(center: CGPoint(x: v[0], y: v[1]), radius: v[2], start: v[3], delta: delta, current: current) + #endif +} + +func _orbCircularArc(center: CGPoint, radius: CGFloat, start: CGFloat, delta: CGFloat, current: CGPoint?) -> [_ORBPathRecord] { + guard radius.isFinite, start.isFinite, delta.isFinite else { return [] } + let segmentCount = ceil(abs(delta) / (.pi / 2)) + guard segmentCount < CGFloat(Int.max) else { return [] } + let count = max(1, Int(segmentCount)) + let step = delta / CGFloat(count) + var angle = start + let first = CGPoint(x: center.x + cos(angle) * radius, y: center.y + sin(angle) * radius) + var result: [_ORBPathRecord] = [] + if current == nil || current! != first { + result.append(.init(element: current == nil ? .moveToPoint : .addLineToPoint, values: [first.x, first.y])) + } + if delta == 0 { return result } + for _ in 0..] + var winding: [Int] + var minimum: SIMD2 + var maximum: SIMD2 + var start = SIMD2.zero + var current = SIMD2.zero + + static func narrow(_ p: CGPoint) -> SIMD2 { SIMD2(Float(p.x), Float(p.y)) } + + mutating func line(_ end: SIMD2) { + let from = current + current = end + let delta = end - from + if abs(delta.y) < 0.0001 { return } + for index in queries.indices { + let t = (queries[index].y - from.y) / delta.y + if t >= 0 && t <= 1 && from.x.addingProduct(delta.x, t) > queries[index].x { + winding[index] += delta.y > 0 ? 1 : -1 + } + } + } + + mutating func close() { + if current != start { line(start) } + } + + mutating func cubic(_ cp1: CGPoint, _ cp2: CGPoint, _ end: CGPoint) { + var a = current + let b = Self.narrow(cp1), c = Self.narrow(cp2), d = Self.narrow(end) + let minX = min(a.x, b.x, c.x, d.x), minY = min(a.y, b.y, c.y, d.y) + let maxX = max(a.x, b.x, c.x, d.x), maxY = max(a.y, b.y, c.y, d.y) + if minY > maximum.y || maxX < minimum.x || maxY < minimum.y { current = d; return } + if minX > maximum.x { line(d); return } + let e = c - (b + b) + a, f = b - (c + c) + d + let x = max(abs(e.x), abs(f.x)), y = max(abs(e.y), abs(f.y)) + let estimate = ceil((x * x + y * y).squareRoot().squareRoot()) + guard estimate.isFinite else { current = d; return } + let segments = Int(min(estimate, 1024)) + if segments >= 2 { + let first = (b - a) * 3 + var second = SIMD2.zero, third = SIMD2.zero + for axis in 0..<2 { + second[axis] = (-first[axis]).addingProduct(3, c[axis] - b[axis]) + third[axis] = d[axis] - (a[axis].addingProduct(3, b[axis] - a[axis]) + second[axis]) + } + let dt = 1 / Float(segments), dt2 = dt * dt, dt3 = dt2 * dt + var delta1 = SIMD2.zero, delta2 = SIMD2.zero + let delta3 = (third * 6) * dt3 + second *= dt2 + for axis in 0..<2 { + delta2[axis] = delta3[axis].addingProduct(2, second[axis]) + delta1[axis] = second[axis].addingProduct(first[axis], dt).addingProduct(third[axis], dt3) + } + for _ in 1.. UInt64 { + var state = _ORBPathWinding(queries: queries.map(_ORBPathWinding.narrow), winding: Array(repeating: 0, count: queries.count), + minimum: SIMD2(repeating: .infinity), maximum: SIMD2(repeating: -.infinity)) + for query in state.queries { + for axis in 0..<2 { + state.minimum[axis] = fmin(state.minimum[axis], query[axis]) + state.maximum[axis] = fmax(state.maximum[axis], query[axis]) + } + } + if state.minimum == state.maximum { + if !bounds.contains(CGPoint(x: CGFloat(state.minimum.x), y: CGFloat(state.minimum.y))) { return 0 } + } else { + let queryBounds = CGRect(x: CGFloat(state.minimum.x), y: CGFloat(state.minimum.y), + width: CGFloat(state.maximum.x - state.minimum.x), height: CGFloat(state.maximum.y - state.minimum.y)) + if !bounds.intersects(queryBounds) { return 0 } + } + for record in records { + switch record.element { + case .moveToPoint: + state.close() + state.current = _ORBPathWinding.narrow(_orbPoint(record.values, 0)) + state.start = state.current + case .addLineToPoint: state.line(_ORBPathWinding.narrow(_orbPoint(record.values, 0))) + case .addQuadCurveToPoint: + let control = _orbPoint(record.values, 0), end = _orbPoint(record.values, 1) + let cp1 = CGPoint(x: CGFloat(state.current.x).addingProduct(2, control.x) / 3, + y: CGFloat(state.current.y).addingProduct(2, control.y) / 3) + let cp2 = CGPoint(x: end.x.addingProduct(2, control.x) / 3, y: end.y.addingProduct(2, control.y) / 3) + state.cubic(cp1, cp2, end) + case .addCurveToPoint: state.cubic(_orbPoint(record.values, 0), _orbPoint(record.values, 1), _orbPoint(record.values, 2)) + case .closeSubpath: state.close() + default: break + } + } + state.close() + var result: UInt64 = 0 + for index in queries.indices { + if eoFill ? state.winding[index] & 1 != 0 : state.winding[index] != 0 { result |= UInt64(1) << index } + } + return result +} + +#endif diff --git a/Sources/OpenRenderBox/Path/ORBPathStorage.swift b/Sources/OpenRenderBox/Path/ORBPathStorage.swift index 2a8a022..a3ab93d 100644 --- a/Sources/OpenRenderBox/Path/ORBPathStorage.swift +++ b/Sources/OpenRenderBox/Path/ORBPathStorage.swift @@ -6,54 +6,65 @@ public import OpenCoreGraphicsShims extension ORBPath.Storage { - public func initialize(capacity: UInt32, source: ORBPath.Storage?) { - _openRenderBoxUnimplementedFailure() + private var _slot: UnsafeMutablePointer { + UnsafeMutableRawPointer(rawValue).assumingMemoryBound(to: UnsafeMutableRawPointer?.self) } - + + private var _records: _ORBPathRecords { + Unmanaged<_ORBPathRecords>.fromOpaque(_slot.pointee!).takeUnretainedValue() + } + + public func initialize(capacity: Int, source: ORBPath.Storage?) { + precondition(capacity >= MemoryLayout.stride) + let records = source?._records.records ?? [] + _slot.initialize(to: Unmanaged.passRetained(_ORBPathRecords(records)).toOpaque()) + } + public func destroy() { - _openRenderBoxUnimplementedFailure() + if let object = _slot.pointee { + _slot.pointee = nil + Unmanaged<_ORBPathRecords>.fromOpaque(object).release() + } + _slot.deinitialize(count: 1) } - + public func clear() { - _openRenderBoxUnimplementedFailure() + _records.records.removeAll(keepingCapacity: true) } - - public func append(element: ORBPath.Element, points: UnsafePointer, userInfo: UnsafeRawPointer?) -> Bool { - _openRenderBoxUnimplementedFailure() + + public func append(element: ORBPath.Element, points: UnsafePointer, userInfo: UnsafeRawPointer?) { + guard let record = _ORBPathRecord(element: element, points: points, userInfo: userInfo) else { return } + _records.records.append(record) } - + + @discardableResult public func append(path: ORBPath) -> Bool { - _openRenderBoxUnimplementedFailure() + guard let records = path._records() else { return false } + _records.records.append(contentsOf: records) + return true } - + public func apply(info: UnsafeMutableRawPointer, callback: ORBPathApplyCallback?) -> Bool { - _openRenderBoxUnimplementedFailure() - } - - public var isEmpty: Bool { - _openRenderBoxUnimplementedFailure() + _records.apply(info: info, callback: callback) } - + + public var isEmpty: Bool { _records.records.isEmpty } + public func isEqual(to rhs: ORBPath.Storage) -> Bool { - _openRenderBoxUnimplementedFailure() - } - - public var isSingleElement: Bool { - _openRenderBoxUnimplementedFailure() + _records.records == rhs._records.records } - - public var bezierOrder: UInt32 { - _openRenderBoxUnimplementedFailure() - } - + + public var isSingleElement: Bool { _orbIsSingleElement(_records.records) } + + public var bezierOrder: Int { _records.records.map(\.order).max() ?? 1 } + public var boundingRect: CGRect { - _openRenderBoxUnimplementedFailure() - } - - public var cgPath: CGPath? { - _openRenderBoxUnimplementedFailure() + let path = ORBPath._fromRecords(_records.records) + defer { path.release() } + return path.boundingRect } + + public var cgPath: CGPath? { _orbMakeCGPath(_records.records) } } #endif - diff --git a/Sources/OpenRenderBoxCxx/Path/ClipStrokeKeyframes.hpp b/Sources/OpenRenderBoxCxx/Path/ClipStrokeKeyframes.hpp new file mode 100644 index 0000000..10f8edb --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/ClipStrokeKeyframes.hpp @@ -0,0 +1,264 @@ +// +// ClipStrokeKeyframes.hpp +// OpenRenderBox + +#pragma once + +#include "PathClipStroke.hpp" + +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN + +#include +#include +#include +#include +#include +#include +#if defined(__aarch64__) +#include +#endif + +namespace ORB::Path::ClipStroke { + +using Keyframe = ClipStrokeKeyframe; + +struct SingleKeyframe { + double distance; + float value; + int32_t sourceIndex; + int32_t tag; +}; +static_assert(sizeof(Keyframe) == 16); +static_assert(sizeof(SingleKeyframe) == 24); + +inline float normalizeProgress(uint32_t flags, float progress) { + if (flags & 2) { + if (!(flags & 4)) progress = std::fmax(progress, 0.0f); + progress = std::fmin(progress, 1.0f); + } + if (flags & 1) progress = 1.0f - progress; + return progress; +} + +inline bool valueClipped(uint32_t flags, float progress, float value) { + if (flags & 1) { + if ((flags & 4) && value >= 1.0f) return false; + return std::fmax(progress, 0.0f) >= value; + } + if ((flags & 4) && value <= 0.0f) return false; + return std::fmin(progress, 1.0f) <= value; +} + +inline float reciprocal(float value) { +#if defined(__aarch64__) + float32x2_t input = vset_lane_f32(value, vdup_n_f32(0), 0); + float32x2_t result = vrecpe_f32(input); + result = vmul_f32(vrecps_f32(input, result), result); + result = vmul_f32(result, vrecps_f32(input, result)); + return vget_lane_f32(result, 0); +#else + return 1.0f / value; +#endif +} + +inline bool makeKeyframes(const Keyframe *input, size_t count, + double subpathLength, float progress, + std::vector &output, double &rate) { + if (count == 0) return false; + output.clear(); + output.reserve(count * 2); + float minimum = std::numeric_limits::infinity(); + float maximum = -minimum; + for (size_t i = 0; i != count; ++i) { + const auto &key = input[i]; + for (float position : {key.start, key.end}) { + const float fraction = position - std::floor(position); + output.push_back({subpathLength * double(fraction), key.value, + int32_t(i), key.tag}); + } + if (key.value < minimum) minimum = key.value; + if (maximum < key.value) maximum = key.value; + } + // Equal distances have no secondary key; the target uses an unstable sort. + std::sort(output.begin(), output.end(), [](const auto &a, const auto &b) { + return a.distance < b.distance; + }); + double totalDistance = 0; + float totalValue = 0; + for (size_t i = 1; i != output.size(); ++i) { + const float difference = std::fabs(output[i].value - output[i - 1].value); + if (!(difference < 0.0001f)) { + totalDistance += output[i].distance; + totalDistance -= output[i - 1].distance; + totalValue += difference; + } + } + rate = totalValue > 0 ? totalDistance * double(reciprocal(totalValue)) : 0; + float previous = output[0].value; + float current = output[1].value; + for (size_t i = 1; i + 1 < output.size(); ++i) { + const float next = output[i + 1].value; + if (minimum < current && current < maximum) { + float low = current < previous ? current : previous; + if (next < low) low = next; + float high = previous < current ? current : previous; + if (high < next) high = next; + if (low > progress || high < progress) + output[i].value = std::numeric_limits::infinity(); + } + // The simplifier compares original neighboring values. + previous = current; + current = next; + } + return !output.empty(); +} + +struct Cursor { + const std::vector &keys; + double subpathLength; + size_t index = 1; + SingleKeyframe previous; + SingleKeyframe next; + + Cursor(const std::vector &values, double length) + : keys(values), subpathLength(length), previous(values.back()), + next(values.front()) { + previous.distance -= length; + } + + float initialValue() const { + const float t = float(previous.distance / (next.distance - previous.distance)); + return std::fma(previous.value - next.value, t, previous.value); + } + + float valueAt(double distance) const { + const float t = float((distance - previous.distance) / + (next.distance - previous.distance)); + return std::fma(next.value - previous.value, t, previous.value); + } + + void advance(double distance) { + while (next.distance <= distance && index < keys.size()) { + const SingleKeyframe candidate = keys[index]; + if (candidate.distance < next.distance) break; + ++index; + if (std::isfinite(candidate.value)) { + previous = next; + next = candidate; + } + } + if (next.distance <= distance && index == keys.size()) { + previous = next; + if (index != 0) { + next = keys.front(); + next.distance += subpathLength; + ++index; + } + } + } +}; + +struct DistanceMap { + struct Entry { + double sourceStart; + double destinationStart; + double sourceLength; + }; + std::vector entries; + + void append(double source, double destination, double length) { + if (!entries.empty()) { + auto &last = entries.back(); + if (std::fabs((last.sourceStart - source) + last.sourceLength) < 1 && + std::fabs((last.sourceLength - destination) + last.destinationStart) < 1) { + last.sourceLength = (length + source) - last.sourceStart; + return; + } + } + entries.push_back({source, destination, length}); + } + + double operator()(double source) const { + const auto found = std::lower_bound(entries.begin(), entries.end(), source, + [](const Entry &entry, double value) { + return (entry.sourceStart + 0.0001) + entry.sourceLength < value; + }); + if (found != entries.end()) { + const double offset = source - found->sourceStart; + if (!(offset < -0.0001)) return found->destinationStart + offset; + } + return std::numeric_limits::infinity(); + } +}; + +inline double signedCircularDifference(float difference) { + if (difference > 0.5f) return double(difference) - 1; + if (difference < -0.5f) return double(difference) + 1; + return double(difference); +} + +struct TaggedSplit { + double firstStart; + float firstEnd; + float secondStart; + double secondEnd; +}; + +inline bool selectTaggedSplit(const std::vector &keys, size_t index, + uint32_t flags, float progress, TaggedSplit &result) { + if (index == 0 || index + 1 >= keys.size() || !(keys[index].tag & 1)) return false; + if (valueClipped(flags, progress, keys[index - 1].value) || + !valueClipped(flags, progress, keys[index + 1].value)) return false; + const auto &key = keys[index]; + const bool reverse = signedCircularDifference(key.end - key.start) < 0; + const float start = reverse ? key.end : key.start; + const float end = reverse ? key.start : key.end; + double nearestBefore = std::numeric_limits::infinity(); + double nearestAfter = nearestBefore; + double pairedBefore = 0; + double pairedAfter = 0; + for (size_t i = 0; i != keys.size(); ++i) { + if (i == index) continue; + const auto &other = keys[i]; + for (int endpoint = 0; endpoint != 2; ++endpoint) { + const float position = endpoint == 0 ? other.start : other.end; + const float pair = endpoint == 0 ? other.end : other.start; + const double before = signedCircularDifference(start - position); + const double after = signedCircularDifference(position - end); + if (before >= 0 && before < nearestBefore) { + nearestBefore = before; + pairedBefore = pair; + } + if (after >= 0 && after < nearestAfter) { + nearestAfter = after; + pairedAfter = pair; + } + } + } + if (!std::isfinite(nearestBefore) || !std::isfinite(nearestAfter)) return false; + result = {pairedBefore, end, start, pairedAfter}; + return true; +} + +inline double positionDistance(double position, double pathLength) { + return (position - std::floor(position)) * pathLength; +} + +inline std::vector remapKeyframes(const std::vector &keys, + size_t splitIndex, double sourceLength, + double closedDestinationLength, + const DistanceMap &map) { + std::vector result; + const double inverseLength = 1 / closedDestinationLength; + for (size_t i = 0; i != keys.size(); ++i) { + Keyframe key = keys[i]; + key.start = float(map(positionDistance(key.start, sourceLength)) * inverseLength); + key.end = float(map(positionDistance(key.end, sourceLength)) * inverseLength); + if (i == splitIndex) key.tag &= ~1; + if (std::isfinite(key.start) && std::isfinite(key.end)) result.push_back(key); + } + return result; +} + +} // namespace ORB::Path::ClipStroke +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/NestedCallbacks.cpp b/Sources/OpenRenderBoxCxx/Path/NestedCallbacks.cpp index f1a0597..946779d 100644 --- a/Sources/OpenRenderBoxCxx/Path/NestedCallbacks.cpp +++ b/Sources/OpenRenderBoxCxx/Path/NestedCallbacks.cpp @@ -2,307 +2,184 @@ // NestedCallbacks.cpp // OpenRenderBox -// TODO: Implement NestedCallbacks - #if OPENRENDERBOX_CF_CGTYPES #include "NestedCallbacks.hpp" -#include -#if ORB_TARGET_OS_DARWIN -#include -#endif #include +#include +#include +#include namespace ORB { namespace Path { - -#if ORB_TARGET_OS_DARWIN - namespace { struct CacheEntry { - const ORBPathCallbacks* callbacks; + const ORBPathCallbacks *callbacks; size_t index; - const NestedCallbacks* result; + const NestedCallbacks *result; }; struct Cache { - ORB::vector entries; - os_unfair_lock lock; - - Cache() : entries(), lock(OS_UNFAIR_LOCK_INIT) {} + ORB::vector entries; + std::mutex lock; }; -Cache* gCache = nullptr; - -// Helper to call bezierOrder on callbacks -inline uint32_t callBezierOrder(const void* object, const ORBPathCallbacks* callbacks) { - if (callbacks->bezierOrder) { - return callbacks->bezierOrder(object); - } - return 3; // Default: cubic bezier -} - -} // anonymous namespace - -NestedCallbacks::NestedCallbacks(const ORBPathCallbacks* callbacks, size_t index) - : extended{} - , originalCallbacks(callbacks) - , nestedIndex(index) -{ - // Set isExtended flag (byte at offset 0x2) - extended.flags.isExtended = 1; - - // Copy retain/release from original callbacks - extended.retain = reinterpret_cast(callbacks->retain); - extended.release = reinterpret_cast(callbacks->release); - - extended.apply = +[](const void *object, void *info, ORBPathApplyCallback callback, const ORBPathCallbacksExtended *ext) -> bool { - auto self = reinterpret_cast(ext); - auto original = self->originalCallbacks; - if (original->apply) { - return original->apply(object, info, callback); - } - return true; +} // namespace + +NestedCallbacks::NestedCallbacks(const ORBPathCallbacks *callbacks, size_t index) + : extended{}, originalCallbacks(callbacks), nestedIndex(index) { + extended.flags.isExtended = true; + extended.retain = callbacks->retain; + extended.release = callbacks->release; + extended.apply = +[](const void *object, void *info, ORBPathApplyCallback callback, const ORBPathCallbacksExtended *extended) { + auto &self = *reinterpret_cast(extended); + ApplyInfo context{info, callback, self.nestedIndex, 0, false}; + ORBPath path{reinterpret_cast(const_cast(object)), self.originalCallbacks}; + bool result = ORBPathApplyElements(path, &context, apply_elements_callback); + return result || context.reachedEnd; }; - - extended.isEqual = nullptr; - - extended.isEmpty = +[](const void *object, const ORBPathCallbacksExtended *ext) -> bool { - auto self = reinterpret_cast(ext); - auto original = self->originalCallbacks; - if (original->apply) { - // Use single_element_callback to detect emptiness - struct Info { - size_t count; - size_t depth; - bool sawClose; - } info = {0, 0, false}; - info.depth = self->nestedIndex; - original->apply(object, &info, NestedCallbacks::single_element_callback); - return info.count == 1; - } - return true; + extended.isSingleElement = +[](const void *object, const ORBPathCallbacksExtended *extended) { + auto &self = *reinterpret_cast(extended); + SingleInfo context{self.nestedIndex}; + ORBPath path{reinterpret_cast(const_cast(object)), self.originalCallbacks}; + ORBPathApplyElements(path, &context, single_element_callback); + return context.count == 1; }; - - extended.bezierOrder = +[](const void *object, const ORBPathCallbacksExtended *ext) -> uint32_t { - auto self = reinterpret_cast(ext); - return callBezierOrder(object, self->originalCallbacks); + extended.bezierOrder = +[](const void *object, const ORBPathCallbacksExtended *extended) -> size_t { + auto &self = *reinterpret_cast(extended); + ORBPath path{reinterpret_cast(const_cast(object)), self.originalCallbacks}; + return ORBPathGetBezierOrder(path); }; - - extended.isSingleElement = nullptr; - extended.boundingRect = nullptr; - extended.cgPath = nullptr; - - extended.next = +[](const void *object, const ORBPathCallbacksExtended *ext) -> const ORBPathCallbacksExtended* { - auto self = reinterpret_cast(ext); - auto nextCallbacks = NestedCallbacks::get(self->originalCallbacks, self->nestedIndex + 1); - return &nextCallbacks->extended; + extended.next = +[](const void *object, const ORBPathCallbacksExtended *extended) -> ORBPath { + auto &self = *reinterpret_cast(extended); + auto *next = NestedCallbacks::get(self.originalCallbacks, self.nestedIndex + 1); + return {reinterpret_cast(const_cast(object)), reinterpret_cast(&next->extended)}; }; } -const NestedCallbacks* NestedCallbacks::get(const ORBPathCallbacks* callbacks, size_t index) { - // Lazy initialization of global cache - static std::once_flag initFlag; - std::call_once(initFlag, []() { - gCache = new Cache(); - }); - - os_unfair_lock_lock(&gCache->lock); - - // Binary search for existing entry - auto& entries = gCache->entries; +const NestedCallbacks *NestedCallbacks::get(const ORBPathCallbacks *callbacks, size_t index) { + // Callback tables are process-wide identities and must remain valid for paths. + static Cache *cache = new Cache; + std::lock_guard guard(cache->lock); + auto &entries = cache->entries; + std::less less; size_t lo = 0; size_t hi = entries.size(); - while (lo < hi) { size_t mid = lo + (hi - lo) / 2; - const auto& entry = entries[mid]; - - if (entry.callbacks < callbacks) { - lo = mid + 1; - } else if (entry.callbacks > callbacks) { - hi = mid; - } else if (entry.index < index) { + const auto &entry = entries[mid]; + if (less(entry.callbacks, callbacks) || (entry.callbacks == callbacks && entry.index < index)) { lo = mid + 1; - } else if (entry.index > index) { + } else if (less(callbacks, entry.callbacks) || (entry.callbacks == callbacks && entry.index > index)) { hi = mid; } else { - // Found exact match - const NestedCallbacks* result = entry.result; - os_unfair_lock_unlock(&gCache->lock); - return result; + return entry.result; } } - - // Not found, create new NestedCallbacks - auto* nested = new NestedCallbacks(callbacks, index); - - // Insert at position 'lo' to maintain sorted order - entries.reserve(entries.size() + 1); - - // Shift elements to make room - if (lo < entries.size()) { - // Use memmove for efficiency - size_t count = entries.size() - lo; - memmove(&entries[lo + 1], &entries[lo], count * sizeof(CacheEntry)); + auto *result = new NestedCallbacks(callbacks, index); + size_t count = entries.size(); + entries.push_back(CacheEntry{}); + if (lo < count) { + std::memmove(&entries[lo + 1], &entries[lo], (count - lo) * sizeof(CacheEntry)); } - - // Insert new entry - entries[lo] = CacheEntry{callbacks, index, nested}; - - os_unfair_lock_unlock(&gCache->lock); - return nested; + entries[lo] = {callbacks, index, result}; + return result; } -// Mask for nesting elements (bits 17-20,21-24): used to detect depth changes -// From assembly: 0x1de0000 = bits for elements 17, 18, 19, 20, 22, 23, 24 -static constexpr uint32_t kNestingElementMask = 0x1de0000; - -// Helper to check if an element affects nesting depth -static inline bool isNestingElement(ORBPathElement element) { - if (element > 0x18) return false; - return ((1u << element) & kNestingElementMask) != 0; -} - -// Helper to check if element is in range 0x0-0x3 (move/line/quad/curve) -static inline bool isDrawingElement(ORBPathElement element) { - return element < 4; +bool NestedCallbacks::is_nesting_element(ORBPathElement element) ORB_NOEXCEPT { + auto type = static_cast(element); + return type <= 24 && ((1u << type) & 0x1de0000) != 0; } -// apply_elements_callback: tracks nesting depth during path enumeration -// Info structure: { context, callback, depth, elementCount, sawClose } -bool NestedCallbacks::apply_elements_callback(void* info, ORBPathElement element, const double* points, const void* userInfo) { - struct ApplyInfo { - void* context; - ORBPathApplyCallback callback; - size_t depth; - }; - auto* applyInfo = static_cast(info); - - if (element > 0x18) { - // Forward to user callback if we're at depth 0 - if (applyInfo->depth == 0) { - return applyInfo->callback(applyInfo->context, element, points, userInfo); +bool NestedCallbacks::apply_elements_callback(void *info, ORBPathElement element, const CGFloat *points, const void *userInfo) { + auto &context = *static_cast(info); + size_t depth = context.currentDepth; + if (is_nesting_element(element)) { + ++context.currentDepth; + } else if (static_cast(element) == 16) { + --context.currentDepth; + if (depth <= context.targetDepth) { + context.reachedEnd = depth == context.targetDepth; + return false; } - return false; - } - - uint32_t mask = 1u << element; - if (mask & kNestingElementMask) { - // Nesting element: increment depth - applyInfo->depth++; - return false; } - - if (element == 0x10) { // Close subpath - if (applyInfo->depth > 0) { - applyInfo->depth--; - } - return false; - } - - // Forward non-nesting elements at depth 0 - if (applyInfo->depth == 0) { - return applyInfo->callback(applyInfo->context, element, points, userInfo); - } - return false; + return depth < context.targetDepth || context.callback(context.context, element, points, userInfo); } -// single_element_callback: checks if path is a single element -// Info structure: { depth, elementCount, sawClose } -bool NestedCallbacks::single_element_callback(void* info, ORBPathElement element, const double* points, const void* userInfo) { - struct SingleInfo { - size_t depth; - size_t count; - bool sawClose; - }; - auto* singleInfo = static_cast(info); - - if (element > 0x18) { - // Elements > 0x18: count as elements at depth 0 - if (singleInfo->depth == singleInfo->count) { - singleInfo->count++; - if (element == 0x13) { // Specific element marker - singleInfo->sawClose = false; - } else { - singleInfo->sawClose = true; - } +bool NestedCallbacks::single_element_callback(void *info, ORBPathElement element, const CGFloat *, const void *) { + auto &context = *static_cast(info); + size_t depth = context.currentDepth; + auto type = static_cast(element); + if (is_nesting_element(element)) { + ++context.currentDepth; + if (depth == context.targetDepth) { + ++context.count; + context.pendingClose = type != 19; } - return false; - } - - uint32_t mask = 1u << element; - - // Handle nesting elements - if ((mask & 0xf000) != 0) { - // Elements 12-15 - if (singleInfo->depth == singleInfo->count) { - singleInfo->sawClose = true; - singleInfo->count++; + } else if (type == 16) { + --context.currentDepth; + if (depth == context.targetDepth) { + return false; } - return false; - } - - if (mask & kNestingElementMask) { - // Other nesting elements: increment depth - if (singleInfo->depth == singleInfo->count) { - singleInfo->count++; + } else if (depth == context.targetDepth) { + if (type < 4 || (type >= 12 && type <= 15)) { + context.pendingClose = true; + ++context.count; + } else if (type == 4) { + if (context.pendingClose) { + context.pendingClose = false; + ++context.count; + } + } else { + ++context.count; } - return false; } + return context.count < 2; +} - if (element == 0x10) { // Close subpath - if (singleInfo->depth > 0) { - singleInfo->depth--; - } - if (singleInfo->depth == singleInfo->count && singleInfo->sawClose) { - singleInfo->sawClose = false; - singleInfo->count++; +bool NestedCallbacks::apply_elements_fast(const Storage &storage, void *info) { + auto &context = *static_cast(info); + return storage.apply_elements_at_depth(context.context, context.callback, context.targetDepth); +} + +bool NestedCallbacks::single_element_fast(const Storage &storage, void *info) { + auto &context = *static_cast(info); + Storage::Iterator iterator; + if (context.targetDepth != 0) { + if (!first_element(storage, iterator, context.targetDepth)) { + return false; } - return false; + context.currentDepth = context.targetDepth; } - - // Drawing elements (0-3) - if (isDrawingElement(element)) { - if (singleInfo->depth == singleInfo->count) { - singleInfo->sawClose = true; - singleInfo->count++; + ORBPathElement element; + while (context.count < 2 && iterator.next_type(storage, element)) { + size_t depth = context.currentDepth; + const CGFloat unused = 0; + single_element_callback(info, element, &unused, nullptr); + if (static_cast(element) == 16 && depth == context.targetDepth) { + return false; } } - - // Return true if count >= 2 (stop enumeration) - return singleInfo->count >= 2; -} - -// apply_elements_fast: fast path for Storage -bool NestedCallbacks::apply_elements_fast(const Storage& storage, void* info) { - // TODO: Implement fast path using Storage iterator - (void)storage; - (void)info; - return true; -} - -// single_element_fast: fast path for single element detection on Storage -bool NestedCallbacks::single_element_fast(const Storage& storage, void* info) { - // TODO: Implement fast path using Storage iterator - (void)storage; - (void)info; return true; } -// first_element: find first element at given nesting depth -bool NestedCallbacks::first_element(const Storage& storage, void* iterator, size_t depth) { - // TODO: Implement using Storage iterator - (void)storage; - (void)iterator; - (void)depth; +bool NestedCallbacks::first_element(const Storage &storage, Storage::Iterator &iterator, size_t depth) { + size_t currentDepth = 0; + ORBPathElement element; + while (iterator.next_type(storage, element)) { + if (is_nesting_element(element)) { + if (++currentDepth == depth) { + return true; + } + } else if (static_cast(element) == 16) { + return false; + } + } return false; } -#endif /* ORB_TARGET_OS_DARWIN */ - -} /* namespace Path */ -} /* namespace ORB */ +} /* Path */ +} /* ORB */ #endif /* OPENRENDERBOX_CF_CGTYPES */ diff --git a/Sources/OpenRenderBoxCxx/Path/NestedCallbacks.hpp b/Sources/OpenRenderBoxCxx/Path/NestedCallbacks.hpp index f27a6a4..b720382 100644 --- a/Sources/OpenRenderBoxCxx/Path/NestedCallbacks.hpp +++ b/Sources/OpenRenderBoxCxx/Path/NestedCallbacks.hpp @@ -9,49 +9,51 @@ #if OPENRENDERBOX_CF_CGTYPES #include +#include "Storage.hpp" ORB_ASSUME_NONNULL_BEGIN namespace ORB { namespace Path { -class Storage; - -/// Extended callbacks wrapper for accessing nested paths. -/// Maintains a global cache to avoid creating duplicate wrappers. struct NestedCallbacks { - ORBPathCallbacksExtended extended; // 0x00-0x57 - const ORBPathCallbacks * _Nonnull originalCallbacks; // 0x58 - size_t nestedIndex; // 0x60 - - /// Get or create a NestedCallbacks for the given callbacks and nesting index - static const NestedCallbacks* get(const ORBPathCallbacks* callbacks, size_t index); - - /// Callback for apply_elements that tracks nesting depth - static bool apply_elements_callback(void* info, ORBPathElement element, const double* points, const void* userInfo); - - /// Callback for single_element detection - static bool single_element_callback(void* info, ORBPathElement element, const double* points, const void* userInfo); - - /// Fast path for applying elements on Storage - static bool apply_elements_fast(const Storage& storage, void* info); - - /// Fast path for single element detection on Storage - static bool single_element_fast(const Storage& storage, void* info); - - /// Find the first element at a given nesting depth - static bool first_element(const Storage& storage, void* iterator, size_t depth); + struct ApplyInfo { + void *context; + ORBPathApplyCallback callback; + size_t targetDepth; + size_t currentDepth; + bool reachedEnd; + }; + + struct SingleInfo { + size_t targetDepth = 0; + size_t currentDepth = 0; + size_t count = 0; + bool pendingClose = false; + }; + + ORBPathCallbacksExtended extended; + const ORBPathCallbacks *originalCallbacks; + size_t nestedIndex; + + static const NestedCallbacks *get(const ORBPathCallbacks *callbacks, size_t index); + static bool is_nesting_element(ORBPathElement element) ORB_NOEXCEPT; + static bool apply_elements_callback(void *info, ORBPathElement element, const CGFloat *points, const void * _Nullable userInfo); + static bool single_element_callback(void *info, ORBPathElement element, const CGFloat *points, const void * _Nullable userInfo); + static bool apply_elements_fast(const Storage &storage, void *info); + static bool single_element_fast(const Storage &storage, void *info); + static bool first_element(const Storage &storage, Storage::Iterator &iterator, size_t depth); private: - NestedCallbacks(const ORBPathCallbacks* callbacks, size_t index); + NestedCallbacks(const ORBPathCallbacks *callbacks, size_t index); }; static_assert(sizeof(NestedCallbacks) == 0x68, "NestedCallbacks size mismatch"); static_assert(offsetof(NestedCallbacks, originalCallbacks) == 0x58, "originalCallbacks offset mismatch"); static_assert(offsetof(NestedCallbacks, nestedIndex) == 0x60, "nestedIndex offset mismatch"); -} /* namespace Path */ -} /* namespace ORB */ +} /* Path */ +} /* ORB */ ORB_ASSUME_NONNULL_END diff --git a/Sources/OpenRenderBoxCxx/Path/ORBPath.cpp b/Sources/OpenRenderBoxCxx/Path/ORBPath.cpp index 30a3449..62ca70c 100644 --- a/Sources/OpenRenderBoxCxx/Path/ORBPath.cpp +++ b/Sources/OpenRenderBoxCxx/Path/ORBPath.cpp @@ -10,7 +10,13 @@ #include #include #include "Storage.hpp" +#include "NestedCallbacks.hpp" +#include "PathMapper.hpp" #include +#include +#include +#include +#include // Empty path callbacks (all null) - C++ internal linkage static const ORBPathCallbacks empty_path_callbacks = { @@ -39,53 +45,240 @@ const ORBPath ORBPathNull = { &empty_path_callbacks, }; -void ORBPathRetain(ORBPath path) { +ORBPath ORBPathRetain(ORBPath path) { auto retain = path.callbacks->retain; if (retain != nullptr) { - retain(&path); + path.storage = reinterpret_cast( + const_cast(retain(path.storage)) + ); } + return path; } void ORBPathRelease(ORBPath path) { auto release = path.callbacks->release; if (release != nullptr) { - release(&path); + release(path.storage); } } #if ORB_TARGET_OS_DARWIN -ORBPath ORBPathCopyTransformedPath(ORBPath path, const CGAffineTransform *transform) { - if (ORBPathIsNull(path)) { - return ORBPathNull; - } - CGPathRef source = ORBPathCopyCGPath(path); - CGPathRef transformed = CGPathCreateCopyByTransformingPath(source, transform); - ORBPath result = ORBPathMakeWithCGPath(transformed); - CGPathRelease(transformed); - CGPathRelease(source); - return result; -} +CG_EXTERN bool CGPathIsPrimitive(CGPathRef path); +CG_EXTERN bool CGPathIsRectWithTransform( + CGPathRef path, + CGRect *rect, + CGAffineTransform *transform +); +CG_EXTERN bool CGPathIsEllipseWithTransform( + CGPathRef path, + CGRect *rect, + CGAffineTransform *transform +); +CG_EXTERN bool CGPathIsRoundedRectWithTransform( + CGPathRef path, + CGRect *rect, + CGFloat *cornerWidth, + CGFloat *cornerHeight, + CGAffineTransform *transform +); +CG_EXTERN void CGPathAddContinuousRoundedRect( + CGMutablePathRef path, + const CGAffineTransform * _Nullable transform, + CGRect rect, + CGFloat cornerWidth, + CGFloat cornerHeight +); + // MARK: - Path Creation namespace { +static_assert(sizeof(ORBPathRectShape) == 0x50); +static_assert(offsetof(ORBPathRectShape, transform) == 0x00); +static_assert(offsetof(ORBPathRectShape, rect) == 0x30); +static_assert(sizeof(ORBPathLineShape) == 0x20); +static_assert(sizeof(ORBPathFixedRoundedRectShape) == 0x68); +static_assert(offsetof(ORBPathFixedRoundedRectShape, cornerSize) == 0x50); +static_assert(offsetof(ORBPathFixedRoundedRectShape, style) == 0x60); +static_assert(sizeof(ORBPathStrokeStyle) == 0x30); +static_assert(offsetof(ORBPathStrokeStyle, dashCount) == 0x20); +static_assert(offsetof(ORBPathStrokeStyle, dash) == 0x28); +static_assert(sizeof(ORBPathStrokedShape) == 0x40); +static_assert(offsetof(ORBPathStrokedShape, style) == 0x10); +static_assert(sizeof(ORBPathShape) == 0x78); + +struct RectPathArguments { + CGRect rect; + uint8_t element; + uint8_t padding[7] {}; +}; + +struct UnevenRoundedRectPathArguments { + CGRect rect; + CGFloat radii[4]; + bool continuous; + uint8_t padding[7] {}; +}; + +struct EllipsePathArguments { + CGRect rect; +}; + +struct RoundedRectPathArguments { + CGRect rect; + CGSize cornerSize; + bool continuous; + uint8_t padding[7] {}; +}; + +static_assert(sizeof(RectPathArguments) == 0x28); +static_assert(sizeof(EllipsePathArguments) == 0x20); +static_assert(sizeof(RoundedRectPathArguments) == 0x38); + +template +struct SingleElementPathStorage { + uint32_t referenceCount { 1 }; + Arguments arguments; + bool hasTransform; + CGAffineTransform transform; + + SingleElementPathStorage( + const Arguments &arguments, + const CGAffineTransform *transform + ) : arguments(arguments), + hasTransform(transform != nullptr && !CGAffineTransformIsIdentity(*transform)), + transform(hasTransform ? *transform : CGAffineTransformIdentity) {} + + const CGAffineTransform *transformPointer() const { + return hasTransform ? &transform : nullptr; + } +}; + +using UnevenRoundedRectPathStorage = SingleElementPathStorage; +using RectPathStorage = SingleElementPathStorage; +using EllipsePathStorage = SingleElementPathStorage; +using RoundedRectPathStorage = SingleElementPathStorage; + +template +const void *retain_single_element_path(const void *object) { + auto storage = const_cast(static_cast(object)); + __atomic_fetch_add(&storage->referenceCount, 1, __ATOMIC_RELAXED); + return object; +} + +template +void release_single_element_path(const void *object) { + auto storage = const_cast(static_cast(object)); + if (__atomic_fetch_sub(&storage->referenceCount, 1, __ATOMIC_ACQ_REL) == 1) { + delete storage; + } +} + +template +bool single_element_paths_equal(const void *object, const void *otherObject) { + auto lhs = static_cast(object); + auto rhs = static_cast(otherObject); + if (lhs->hasTransform != rhs->hasTransform + || std::memcmp(&lhs->arguments, &rhs->arguments, sizeof(lhs->arguments)) != 0) { + return false; + } + return !lhs->hasTransform + || CGAffineTransformEqualToTransform(lhs->transform, rhs->transform); +} + +static const ORBPathCallbacks rect_path_callbacks = { + .flags = {}, + .retain = retain_single_element_path, + .release = release_single_element_path, + .apply = +[](const void *object, void *info, ORBPathApplyCallback callback) -> bool { + if (callback == nullptr) { + return true; + } + auto storage = static_cast(object); + return callback( + info, + static_cast(storage->arguments.element), + reinterpret_cast(&storage->arguments.rect), + storage->transformPointer() + ); + }, + .isEqual = single_element_paths_equal, + .isEmpty = +[](const void *) -> bool { return false; }, + .isSingleElement = +[](const void *) -> bool { return true; }, + .bezierOrder = +[](const void *) -> size_t { return 1; }, + .boundingRect = nullptr, + .cgPath = nullptr, + .next = nullptr, +}; + +static const ORBPathCallbacks ellipse_path_callbacks = { + .flags = {}, + .retain = retain_single_element_path, + .release = release_single_element_path, + .apply = +[](const void *object, void *info, ORBPathApplyCallback callback) -> bool { + if (callback == nullptr) { + return true; + } + auto storage = static_cast(object); + return callback( + info, + ORBPathElementEllipse, + reinterpret_cast(&storage->arguments.rect), + storage->transformPointer() + ); + }, + .isEqual = single_element_paths_equal, + .isEmpty = +[](const void *) -> bool { return false; }, + .isSingleElement = +[](const void *) -> bool { return true; }, + .bezierOrder = +[](const void *) -> size_t { return 3; }, + .boundingRect = nullptr, + .cgPath = nullptr, + .next = nullptr, +}; + +static const ORBPathCallbacks rounded_rect_path_callbacks = { + .flags = {}, + .retain = retain_single_element_path, + .release = release_single_element_path, + .apply = +[](const void *object, void *info, ORBPathApplyCallback callback) -> bool { + if (callback == nullptr) { + return true; + } + auto storage = static_cast(object); + return callback( + info, + storage->arguments.continuous + ? ORBPathElementFixedRoundedRectContinuous + : ORBPathElementFixedRoundedRectCircular, + reinterpret_cast(&storage->arguments.rect), + storage->transformPointer() + ); + }, + .isEqual = single_element_paths_equal, + .isEmpty = +[](const void *) -> bool { return false; }, + .isSingleElement = +[](const void *) -> bool { return true; }, + .bezierOrder = +[](const void *) -> size_t { return 3; }, + .boundingRect = nullptr, + .cgPath = nullptr, + .next = nullptr, +}; + ORBPath make_rect(CGRect rect, const CGAffineTransform *transform, ORBPathElement element) { if (CGRectIsNull(rect)) { - return ORBPathNull; - } - if (transform == nullptr || CGAffineTransformIsIdentity(*transform)) { - // TODO - return ORBPathNull; - } else { - // TODO - return ORBPathNull; + return ORBPathEmpty; } + RectPathArguments arguments = {}; + arguments.rect = rect; + arguments.element = static_cast(element); + auto storage = new RectPathStorage(arguments, transform); + return ORBPath { + reinterpret_cast(storage), + &rect_path_callbacks, + }; } } /* anonymous namespace */ -// TODO: TO be implemented natively - ORBPath ORBPathMakeWithCGPath(CGPathRef cgPath) { if (cgPath == nullptr) { return ORBPathNull; @@ -102,60 +295,147 @@ ORBPath ORBPathMakeRect(CGRect rect, const CGAffineTransform *transform) { } ORBPath ORBPathMakeEllipse(CGRect rect, const CGAffineTransform *transform) { - CGPathRef cgPath = CGPathCreateWithEllipseInRect(rect, transform); - ORBPath path = { - reinterpret_cast(const_cast(cgPath)), - &ORBPathCGPathCallbacks, + if (CGRectIsNull(rect)) { + return ORBPathEmpty; + } + EllipsePathArguments arguments = { rect }; + auto storage = new EllipsePathStorage(arguments, transform); + return ORBPath { + reinterpret_cast(storage), + &ellipse_path_callbacks, }; - return path; } ORBPath ORBPathMakeRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, ORBPathRoundedCornerStyle style, const CGAffineTransform *transform) { - // TODO: Handle ORBRoundedCornerStyleContinuous with custom bezier curves - CGPathRef cgPath = CGPathCreateWithRoundedRect(rect, cornerWidth, cornerHeight, transform); - ORBPath path = { - reinterpret_cast(const_cast(cgPath)), - &ORBPathCGPathCallbacks, + if (cornerWidth == 0 && cornerHeight == 0) { + return make_rect(rect, transform, ORBPathElementRoundedRect); + } + if (CGRectIsNull(rect)) { + return ORBPathEmpty; + } + RoundedRectPathArguments arguments = {}; + arguments.rect = rect; + arguments.cornerSize = CGSizeMake(cornerWidth, cornerHeight); + arguments.continuous = style == ORBPathRoundedCornerStyleContinuous; + auto storage = new RoundedRectPathStorage(arguments, transform); + return ORBPath { + reinterpret_cast(storage), + &rounded_rect_path_callbacks, }; - return path; } -ORBPath ORBPathMakeUnevenRoundedRect(CGRect rect, CGFloat topLeftRadius, CGFloat bottomLeftRadius, CGFloat bottomRightRadius, CGFloat topRightRadius, ORBPathRoundedCornerStyle style, const CGAffineTransform *transform) { - // TODO: Handle ORBRoundedCornerStyleContinuous with custom bezier curves - CGMutablePathRef cgPath = CGPathCreateMutable(); - - CGFloat minX = CGRectGetMinX(rect); - CGFloat minY = CGRectGetMinY(rect); - CGFloat maxX = CGRectGetMaxX(rect); - CGFloat maxY = CGRectGetMaxY(rect); - - // Start at top-left corner (after the rounded corner) - CGPathMoveToPoint(cgPath, transform, minX + topLeftRadius, minY); +ORBPath ORBPathMakeUnevenRoundedRect(CGRect rect, CGFloat topLeftRadius, CGFloat topRightRadius, CGFloat bottomRightRadius, CGFloat bottomLeftRadius, ORBPathRoundedCornerStyle style, const CGAffineTransform *transform) { + if (CGRectIsNull(rect)) { + return ORBPathEmpty; + } + if (topLeftRadius == 0 && topRightRadius == 0 && bottomRightRadius == 0 && bottomLeftRadius == 0) { + return make_rect(rect, transform, ORBPathElementRoundedRect); + } + static const ORBPathCallbacks callbacks = { + .flags = {}, + .retain = retain_single_element_path, + .release = release_single_element_path, + .apply = +[](const void *object, void *info, ORBPathApplyCallback callback) -> bool { + auto storage = static_cast(object); + return callback(info, storage->arguments.continuous + ? ORBPathElementUnevenRoundedRectContinuous : ORBPathElementUnevenRoundedRectCircular, + reinterpret_cast(&storage->arguments.rect), storage->transformPointer()); + }, + .isEqual = single_element_paths_equal, + .isEmpty = +[](const void *) -> bool { return false; }, + .isSingleElement = +[](const void *) -> bool { return true; }, + .bezierOrder = +[](const void *) -> size_t { return 3; }, + .boundingRect = nullptr, + .cgPath = nullptr, + .next = nullptr, + }; + UnevenRoundedRectPathArguments args = {rect, {topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius}, style == ORBPathRoundedCornerStyleContinuous}; + return {reinterpret_cast(new UnevenRoundedRectPathStorage(args, transform)), &callbacks}; +} - // Top edge and top-right corner - CGPathAddLineToPoint(cgPath, transform, maxX - topRightRadius, minY); - CGPathAddArc(cgPath, transform, maxX - topRightRadius, minY + topRightRadius, topRightRadius, -M_PI_2, 0, false); +namespace { +struct WrappedPathStorage { + uint32_t referenceCount = 1; + ORBPath path; + ORBPathElement element; + CGAffineTransform transform = CGAffineTransformIdentity; + CGFloat values[3] = {}; + int32_t cap = 0; + int32_t join = 0; + std::vector dash; + + WrappedPathStorage(ORBPath path, ORBPathElement element) + : path(ORBPathRetain(path)), element(element) {} + ~WrappedPathStorage() { ORBPathRelease(path); } +}; - // Right edge and bottom-right corner - CGPathAddLineToPoint(cgPath, transform, maxX, maxY - bottomRightRadius); - CGPathAddArc(cgPath, transform, maxX - bottomRightRadius, maxY - bottomRightRadius, bottomRightRadius, 0, M_PI_2, false); +const ORBPathCallbacks wrapped_path_callbacks = { + .flags = {}, + .retain = retain_single_element_path, + .release = release_single_element_path, + .apply = +[](const void *object, void *info, ORBPathApplyCallback callback) -> bool { + auto &storage = *static_cast(object); + ORB::Path::Storage::StrokeInfo stroke{storage.cap, storage.join, storage.dash.size(), storage.dash.data()}; + const void *metadata = storage.element == ORBPathElementTransformed ? static_cast(&storage.transform) + : storage.element == ORBPathElementStroked ? &stroke : nullptr; + return callback(info, storage.element, storage.values, metadata) + && ORBPathApplyElements(storage.path, info, callback) + && callback(info, ORBPathElementEnd, storage.values, nullptr); + }, + .isEqual = +[](const void *a, const void *b) -> bool { + auto &lhs = *static_cast(a); + auto &rhs = *static_cast(b); + return lhs.element == rhs.element && CGAffineTransformEqualToTransform(lhs.transform, rhs.transform) + && std::equal(lhs.values, lhs.values + 3, rhs.values) && lhs.cap == rhs.cap && lhs.join == rhs.join + && lhs.dash == rhs.dash && ORBPathEqualToPath(lhs.path, rhs.path); + }, + .isEmpty = +[](const void *) -> bool { return false; }, + .isSingleElement = +[](const void *) -> bool { return true; }, + .bezierOrder = nullptr, + .boundingRect = nullptr, + .cgPath = nullptr, + .next = +[](const void *object) -> ORBPath { return static_cast(object)->path; }, +}; - // Bottom edge and bottom-left corner - CGPathAddLineToPoint(cgPath, transform, minX + bottomLeftRadius, maxY); - CGPathAddArc(cgPath, transform, minX + bottomLeftRadius, maxY - bottomLeftRadius, bottomLeftRadius, M_PI_2, M_PI, false); +ORBPath nested_path(ORBPath path) { + if (path.callbacks->flags.isExtended) { + auto *extended = reinterpret_cast(path.callbacks); + if (extended->next) { return extended->next(path.storage, extended); } + } else if (path.callbacks->next) { + return path.callbacks->next(path.storage); + } + auto *callbacks = ORB::Path::NestedCallbacks::get(path.callbacks, 1); + return {path.storage, reinterpret_cast(&callbacks->extended)}; +} +} // namespace - // Left edge and top-left corner - CGPathAddLineToPoint(cgPath, transform, minX, minY + topLeftRadius); - CGPathAddArc(cgPath, transform, minX + topLeftRadius, minY + topLeftRadius, topLeftRadius, M_PI, M_PI + M_PI_2, false); +ORBPath ORBPathCopyTransformedPath(ORBPath path, const CGAffineTransform *transform) { + if (path.callbacks == &empty_path_callbacks) { return path; } + auto *storage = new WrappedPathStorage(path, ORBPathElementTransformed); + storage->transform = transform ? *transform : CGAffineTransformIdentity; + return {reinterpret_cast(storage), &wrapped_path_callbacks}; +} - CGPathCloseSubpath(cgPath); +ORBPath ORBPathCopyStrokedPath(ORBPath path, CGFloat width, int32_t cap, int32_t join, CGFloat miter, CGFloat phase, const CGFloat *dash, size_t count) { + if (path.callbacks == &empty_path_callbacks) { return ORBPathEmpty; } + auto *storage = new WrappedPathStorage(path, ORBPathElementStroked); + storage->values[0] = width; + storage->values[1] = miter; + storage->values[2] = phase; + storage->cap = cap; + storage->join = join; + if (dash && count) { storage->dash.assign(dash, dash + count); } + return {reinterpret_cast(storage), &wrapped_path_callbacks}; +} - ORBPath path = { - reinterpret_cast(cgPath), - &ORBPathCGPathCallbacks, - }; - return path; +ORBPath ORBPathCopyTrimmedPath(ORBPath path, CGFloat from, CGFloat to) { + if (path.callbacks == &empty_path_callbacks) { return ORBPathEmpty; } + auto *storage = new WrappedPathStorage(path, ORBPathElementTrimmed); + storage->values[0] = from; + storage->values[1] = to; + return {reinterpret_cast(storage), &wrapped_path_callbacks}; } + #endif /* ORB_TARGET_OS_DARWIN */ bool ORBPathIsEmpty(ORBPath path) { @@ -196,6 +476,32 @@ bool ORBPathIsEmpty(ORBPath path) { } } +bool ORBPathIsSingleElement(ORBPath path) { + if (path.callbacks == &empty_path_callbacks) { + return false; + } + if (path.callbacks->flags.isExtended) { + auto extended = reinterpret_cast(path.callbacks); + if (extended->isSingleElement != nullptr) { + return extended->isSingleElement(path.storage, extended); + } + } else if (path.callbacks->isSingleElement != nullptr) { + return path.callbacks->isSingleElement(path.storage); + } + + ORB::Path::NestedCallbacks::SingleInfo count; + ORBPathApplyElements(path, &count, ORB::Path::NestedCallbacks::single_element_callback); + return count.count == 1; +} + +size_t ORBPathGetBezierOrder(ORBPath path) { + if (path.callbacks->flags.isExtended) { + auto *extended = reinterpret_cast(path.callbacks); + return extended->bezierOrder ? extended->bezierOrder(path.storage, extended) : 3; + } + return path.callbacks->bezierOrder ? path.callbacks->bezierOrder(path.storage) : 3; +} + bool ORBPathApplyElements(ORBPath path, void *info, ORBPathApplyCallback callback) { if (callback == nullptr) { return true; @@ -216,58 +522,467 @@ bool ORBPathApplyElements(ORBPath path, void *info, ORBPathApplyCallback callbac } } -bool ORBPathEqualToPath(ORBPath lhs, ORBPath rhs) { - if (lhs.callbacks == rhs.callbacks) { - if (lhs.storage == rhs.storage) { +bool ORBPathApplyShape( + ORBPath path, + bool allowLine, + void *info, + ORBPathApplyShapeCallback callback +) { + if (callback == nullptr) { + return false; + } + +#if ORB_TARGET_OS_DARWIN + if (path.callbacks == &ORBPathCGPathCallbacks) { + auto cgPath = reinterpret_cast(path.storage); + if (!CGPathIsPrimitive(cgPath)) { + return false; + } + + CGRect rect = CGRectZero; + CGAffineTransform transform = CGAffineTransformIdentity; + ORBPathShape shape = {}; + if (CGPathIsRectWithTransform(cgPath, &rect, &transform)) { + shape.rect = { transform, rect }; + callback(info, ORBPathShapeTypeRect, &shape); return true; } - auto callbacks = lhs.callbacks; - if (callbacks->flags.isExtended) { - auto extended = reinterpret_cast(callbacks); - auto isEqualCallback = extended->isEqual; - if (isEqualCallback) { - return isEqualCallback(lhs.storage, rhs.storage, extended); - } - } else { - auto isEqualCallback = callbacks->isEqual; - if (isEqualCallback) { - return isEqualCallback(lhs.storage, rhs.storage); + if (CGPathIsEllipseWithTransform(cgPath, &rect, &transform)) { + shape.ellipse = { transform, rect }; + callback(info, ORBPathShapeTypeEllipse, &shape); + return true; + } + + CGFloat cornerWidth = 0; + CGFloat cornerHeight = 0; + if (CGPathIsRoundedRectWithTransform( + cgPath, + &rect, + &cornerWidth, + &cornerHeight, + &transform + )) { + if (cornerWidth == 0 && cornerHeight == 0) { + shape.rect = { transform, rect }; + callback(info, ORBPathShapeTypeRect, &shape); + return true; } + shape.fixedRoundedRect = { + transform, + rect, + CGSizeMake(cornerWidth, cornerHeight), + ORBPathRoundedCornerStyleCircular, + }; + callback(info, ORBPathShapeTypeFixedRoundedRect, &shape); + return true; } - // auto storage = ORB::Path::Storage(1088); - // TODO return false; - } else { - if (lhs.callbacks == &empty_path_callbacks) { - if (lhs.storage == ORBPathNull.storage) { - return rhs.callbacks == &empty_path_callbacks && rhs.storage == ORBPathNull.storage; - } else { - return ORBPathIsEmpty(rhs); + } +#endif + + if (ORBPathIsSingleElement(path)) { + struct ShapeInfo { + void *info; + ORBPathApplyShapeCallback callback; + ORBPath path; + bool matched; + } shapeInfo = { info, callback, path, false }; + + ORBPathApplyElements(path, &shapeInfo, +[]( + void *rawInfo, + ORBPathElement element, + const CGFloat *values, + const void *userInfo + ) -> bool { + auto info = static_cast(rawInfo); + if (element == ORBPathElementTransformed || element == ORBPathElementTrimmed || element == ORBPathElementStroked) { + ORBPathShape shape = {}; + auto nested = nested_path(info->path); + ORBPathShapeType type; + if (element == ORBPathElementTransformed) { + shape.nested = {nested, userInfo ? *static_cast(userInfo) : CGAffineTransformIdentity}; + type = ORBPathShapeTypeNested; + } else if (element == ORBPathElementTrimmed) { + shape.trimmed = {nested, values[0], values[1]}; + type = ORBPathShapeTypeTrimmed; + } else { + ORB::Path::Storage::StrokeInfo stroke = {}; + if (userInfo) { stroke = *static_cast(userInfo); } + static const CGFloat emptyDash = 0; + size_t count = stroke.values ? stroke.count : 0; + auto *dash = count ? static_cast(stroke.values) : &emptyDash; + shape.stroked = {nested, {values[0], stroke.cap, stroke.join, values[1], values[2], count, dash}}; + type = ORBPathShapeTypeStroked; + } + info->callback(info->info, type, &shape); + info->matched = true; + return false; } - } else if (rhs.callbacks == &empty_path_callbacks) { - if (rhs.storage == ORBPathNull.storage) { + switch (element) { + case ORBPathElementRect: + case ORBPathElementRoundedRect: + case ORBPathElementEllipse: + case ORBPathElementFixedRoundedRectCircular: + case ORBPathElementFixedRoundedRectContinuous: + case ORBPathElementUnevenRoundedRectCircular: + case ORBPathElementUnevenRoundedRectContinuous: + break; + default: return false; - } else { - return ORBPathIsEmpty(lhs); } + CGAffineTransform transform = userInfo == nullptr + ? CGAffineTransformIdentity + : *static_cast(userInfo); + CGRect rect = CGRectMake(values[0], values[1], values[2], values[3]); + ORBPathShape shape = {}; + ORBPathShapeType type; + switch (element) { + case ORBPathElementRect: + case ORBPathElementRoundedRect: + shape.rect = { transform, rect }; + type = ORBPathShapeTypeRect; + break; + case ORBPathElementEllipse: + shape.ellipse = { transform, rect }; + type = ORBPathShapeTypeEllipse; + break; + case ORBPathElementFixedRoundedRectCircular: + case ORBPathElementFixedRoundedRectContinuous: + shape.fixedRoundedRect = { + transform, + rect, + CGSizeMake(values[4], values[5]), + element == ORBPathElementFixedRoundedRectContinuous + ? ORBPathRoundedCornerStyleContinuous + : ORBPathRoundedCornerStyleCircular, + }; + type = ORBPathShapeTypeFixedRoundedRect; + break; + case ORBPathElementUnevenRoundedRectCircular: + case ORBPathElementUnevenRoundedRectContinuous: + shape.unevenRoundedRect = { + transform, + rect, + values[4], values[5], values[6], values[7], + element == ORBPathElementUnevenRoundedRectContinuous + ? ORBPathRoundedCornerStyleContinuous + : ORBPathRoundedCornerStyleCircular, + }; + type = ORBPathShapeTypeUnevenRoundedRect; + break; + default: + return false; + } + info->callback(info->info, type, &shape); + info->matched = true; + return false; + }); + return shapeInfo.matched; + } + + // RenderBox's 6.5.4 implementation takes this branch when the ABI + // parameter named `allowLine` is false. + if (!allowLine) { + struct LineInfo { + CGPoint points[2]; + uint64_t count; + } lineInfo = {}; + + bool applied = ORBPathApplyElements(path, &lineInfo, +[]( + void *rawInfo, + ORBPathElement element, + const CGFloat *values, + const void *userInfo + ) -> bool { + auto info = static_cast(rawInfo); + if (element == ORBPathElementMoveToPoint) { + if (info->count > 1) { + return false; + } + info->count = 0; + } else if (element != ORBPathElementAddLineToPoint) { + return false; + } + if (info->count != static_cast(element)) { + return false; + } + CGPoint point = reinterpret_cast(values)[0]; + if (userInfo != nullptr) { + point = CGPointApplyAffineTransform( + point, + *static_cast(userInfo) + ); + } + info->points[info->count++] = point; + return true; + }); + if (applied && lineInfo.count == 2) { + ORBPathShape shape = {}; + shape.line = { lineInfo.points[0], lineInfo.points[1] }; + callback(info, ORBPathShapeTypeLine, &shape); + return true; } } + + return false; +} + +bool ORBPathEqualToPath(ORBPath lhs, ORBPath rhs) { + if (lhs.callbacks == rhs.callbacks) { + if (lhs.storage == rhs.storage) { return true; } + if (lhs.callbacks->flags.isExtended) { + auto *extended = reinterpret_cast(lhs.callbacks); + if (extended->isEqual) { return extended->isEqual(lhs.storage, rhs.storage, extended); } + } else if (lhs.callbacks->isEqual) { + return lhs.callbacks->isEqual(lhs.storage, rhs.storage); + } + } else { + if (lhs.callbacks == &empty_path_callbacks) { + return lhs.storage == ORBPathNull.storage ? false : ORBPathIsEmpty(rhs); + } + if (rhs.callbacks == &empty_path_callbacks) { + return rhs.storage == ORBPathNull.storage ? false : ORBPathIsEmpty(lhs); + } + } + using Storage = ORB::Path::Storage; + // Only the left stream is normalized by Storage, matching the callback ABI. + Storage storage(sizeof(Storage)); + ORBPathApplyElements(lhs, &storage, ORB::Path::append_element_callback); + struct Comparison { + const Storage &storage; + Storage::Iterator iterator; + } comparison{storage, {}}; + bool equal = ORBPathApplyElements(rhs, &comparison, +[](void *context, ORBPathElement type, const CGFloat *points, const void *info) -> bool { + auto &comparison = *static_cast(context); + Storage::Element element; + if (!comparison.iterator.next(comparison.storage, element) || type != element.type) { return false; } + for (size_t index = 0; index < Storage::argument_count(type); ++index) { + if (element.points[index] != points[index]) { return false; } + } + if (!info && !element.hasInfo) { return true; } + switch (type) { + case ORBPathElementCloseSubpath: + case ORBPathElementEnd: + case ORBPathElementTrimmed: + case ORBPathElementDilateGlyph: + case ORBPathElementOffset: + return true; + case ORBPathElementStroked: + case ORBPathElementClipStroke: { + Storage::StrokeInfo defaults{0, type == ORBPathElementClipStroke ? 1 : 0, 0, nullptr}; + auto &a = element.hasInfo ? element.metadata.stroke : defaults; + auto &b = info ? *static_cast(info) : defaults; + if (a.cap != b.cap || a.join != b.join || a.count != b.count) { return false; } + if (type == ORBPathElementStroked) { + auto *av = static_cast(a.values), *bv = static_cast(b.values); + for (size_t index = 0; index < a.count; ++index) { if (av[index] != bv[index]) { return false; } } + } else { + auto *av = static_cast(a.values), *bv = static_cast(b.values); + for (size_t index = 0; index < a.count; ++index) { + if (av[index].type != bv[index].type) { return false; } + float ap[3], bp[3]; + std::memcpy(ap, av[index].values, sizeof(ap)); + std::memcpy(bp, bv[index].values, sizeof(bp)); + if (!std::equal(ap, ap + 3, bp)) { return false; } + } + } + return true; + } + case ORBPathElementBoolean: { + const uint8_t defaults[2] = {}; + auto *a = element.hasInfo ? element.metadata.boolean : defaults; + auto *b = info ? static_cast(info) : defaults; + return a[0] == b[0] && a[1] == b[1]; + } + default: { + CGAffineTransform a = CGAffineTransformIdentity; + if (element.hasInfo) { std::memcpy(&a, element.metadata.transform, sizeof(a)); } + auto &b = info ? *static_cast(info) : CGAffineTransformIdentity; + return CGAffineTransformEqualToTransform(a, b); + } + } + }); + return equal && comparison.iterator.offset >= storage.actual_size(); } #if ORB_TARGET_OS_DARWIN CGPathRef ORBPathCopyCGPath(ORBPath path) { - // TODO: Return a retained copy of the CGPath - return nullptr; + if (path.callbacks == &empty_path_callbacks) { + return CGPathCreateMutable(); + } + + CGPathRef result = nullptr; + if (path.callbacks->flags.isExtended) { + auto extended = reinterpret_cast(path.callbacks); + if (extended->cgPath != nullptr) { + result = extended->cgPath(path.storage, extended); + } + } else if (path.callbacks->cgPath != nullptr) { + result = path.callbacks->cgPath(path.storage); + } + if (result != nullptr) { + return CGPathRetain(result); + } + + return ORB::Path::copy_cg_path(path); +} + +CGRect ORBPathGetBoundingRect(ORBPath path) { + if (path.callbacks->flags.isExtended) { + auto extended = reinterpret_cast(path.callbacks); + if (extended->boundingRect != nullptr) { + return extended->boundingRect(path.storage, extended); + } + } else if (path.callbacks->boundingRect != nullptr) { + return path.callbacks->boundingRect(path.storage); + } + + struct PrimitiveBounds { CGRect rect; bool matched; } bounds{CGRectNull, false}; + if (ORBPathIsSingleElement(path)) { + ORBPathApplyShape(path, true, &bounds, +[](void *context, ORBPathShapeType type, const ORBPathShape *shape) { + auto &bounds = *static_cast(context); + switch (type) { + case ORBPathShapeTypeRect: + case ORBPathShapeTypeEllipse: + case ORBPathShapeTypeFixedRoundedRect: + case ORBPathShapeTypeUnevenRoundedRect: + bounds.rect = CGRectApplyAffineTransform(shape->rect.rect, shape->rect.transform); + bounds.matched = true; + break; + case ORBPathShapeTypeNested: + bounds.rect = CGRectApplyAffineTransform(ORBPathGetBoundingRect(shape->nested.path), shape->nested.transform); + bounds.matched = true; + break; + default: break; + } + }); + if (bounds.matched) { return bounds.rect; } + } + + CGPathRef cgPath = ORBPathCopyCGPath(path); + CGRect result = CGPathGetPathBoundingBox(cgPath); + CGPathRelease(cgPath); + return result; } bool ORBPathContainsPoint(ORBPath path, CGPoint point, bool eoFill) { return ORBPathContainsPoints(path, 1, &point, eoFill, nullptr); } -bool ORBPathContainsPoints(ORBPath path, uint64_t count, const CGPoint *points, bool eoFill, const CGAffineTransform *transform) { - // TODO: Implement point containment testing with winding rule - return false; +namespace { +using FloatPoint = float __attribute__((ext_vector_type(2))); + +struct PathWinding { + FloatPoint queries[64]; + int winding[64] = {}; + size_t count; + FloatPoint minimum, maximum; + FloatPoint start = {}, current = {}; + + static FloatPoint narrow(CGPoint p) { return {static_cast(p.x), static_cast(p.y)}; } + + void line(FloatPoint end) { + auto from = current; + current = end; + auto delta = end - from; + if (std::abs(delta.y) < 0.0001f) { return; } + for (size_t i = 0; i < count; ++i) { + float t = (queries[i].y - from.y) / delta.y; + if (t >= 0 && t <= 1 && std::fma(delta.x, t, from.x) > queries[i].x) { + winding[i] += delta.y > 0 ? 1 : -1; + } + } + } + + void close() { + if (current.x != start.x || current.y != start.y) { line(start); } + } + + void cubic(CGPoint cp1, CGPoint cp2, CGPoint endpoint) { + auto a = current, b = narrow(cp1), c = narrow(cp2), d = narrow(endpoint); + float minX = std::min({a.x, b.x, c.x, d.x}), minY = std::min({a.y, b.y, c.y, d.y}); + float maxX = std::max({a.x, b.x, c.x, d.x}), maxY = std::max({a.y, b.y, c.y, d.y}); + if (minY > maximum.y || maxX < minimum.x || maxY < minimum.y) { current = d; return; } + if (minX > maximum.x) { line(d); return; } + auto e = c - (b + b) + a, f = b - (c + c) + d; + float x = std::max(std::abs(e.x), std::abs(f.x)); + float y = std::max(std::abs(e.y), std::abs(f.y)); + float estimate = std::ceil(std::sqrt(std::sqrt(x * x + y * y))); + if (!std::isfinite(estimate)) { current = d; return; } + int segments = static_cast(std::min(estimate, 1024.0f)); + if (segments >= 2) { + auto first = (b - a) * 3.0f; + FloatPoint second, third; + for (size_t axis = 0; axis != 2; ++axis) { + second[axis] = std::fma(3.0f, c[axis] - b[axis], -first[axis]); + third[axis] = d[axis] - (std::fma(3.0f, b[axis] - a[axis], a[axis]) + second[axis]); + } + float dt = 1.0f / segments, dt2 = dt * dt, dt3 = dt2 * dt; + FloatPoint delta1, delta2, delta3 = (third * 6.0f) * dt3; + second *= dt2; + for (size_t axis = 0; axis != 2; ++axis) { + delta2[axis] = std::fma(2.0f, second[axis], delta3[axis]); + delta1[axis] = std::fma(third[axis], dt3, std::fma(first[axis], dt, second[axis])); + } + for (int step = 1; step < segments; ++step) { + a += delta1; + delta1 += delta2; + delta2 += delta3; + line(a); + } + } + line(d); + } +}; +} // namespace + +uint64_t ORBPathContainsPoints(ORBPath path, uint64_t count, const CGPoint *points, bool eoFill, const CGAffineTransform *transform) { + if (count == 0) { return 0; } + PathWinding state{}; + state.count = std::min(count, 64); + state.minimum = {INFINITY, INFINITY}; + state.maximum = {-INFINITY, -INFINITY}; + for (size_t i = 0; i < state.count; ++i) { + CGPoint point = transform ? CGPointApplyAffineTransform(points[i], *transform) : points[i]; + state.queries[i] = PathWinding::narrow(point); + for (size_t axis = 0; axis != 2; ++axis) { + state.minimum[axis] = std::fmin(state.minimum[axis], state.queries[i][axis]); + state.maximum[axis] = std::fmax(state.maximum[axis], state.queries[i][axis]); + } + } + auto bounds = ORBPathGetBoundingRect(path); + bool pointQuery = state.minimum.x == state.maximum.x && state.minimum.y == state.maximum.y; + CGRect queryBounds = CGRectMake(state.minimum.x, state.minimum.y, state.maximum.x - state.minimum.x, state.maximum.y - state.minimum.y); + if (pointQuery ? !CGRectContainsPoint(bounds, CGPointMake(state.minimum.x, state.minimum.y)) : !CGRectIntersectsRect(bounds, queryBounds)) { return 0; } + auto cgPath = ORBPathCopyCGPath(path); + CGPathApply(cgPath, &state, +[](void *info, const CGPathElement *element) { + auto &state = *static_cast(info); + auto *p = element->points; + switch (element->type) { + case kCGPathElementMoveToPoint: + state.close(); + state.current = state.start = PathWinding::narrow(p[0]); + break; + case kCGPathElementAddLineToPoint: state.line(PathWinding::narrow(p[0])); break; + case kCGPathElementAddQuadCurveToPoint: { + CGPoint c1 = {std::fma(2.0, p[0].x, state.current.x) / 3, std::fma(2.0, p[0].y, state.current.y) / 3}; + CGPoint c2 = {std::fma(2.0, p[0].x, p[1].x) / 3, std::fma(2.0, p[0].y, p[1].y) / 3}; + state.cubic(c1, c2, p[1]); + break; + } + case kCGPathElementAddCurveToPoint: state.cubic(p[0], p[1], p[2]); break; + case kCGPathElementCloseSubpath: state.close(); break; + } + }); + state.close(); + CGPathRelease(cgPath); + uint64_t result = 0; + for (size_t i = 0; i < state.count; ++i) { + if (eoFill ? (state.winding[i] & 1) != 0 : state.winding[i] != 0) { result |= uint64_t(1) << i; } + } + return result; } + #endif /* ORB_TARGET_OS_DARWIN */ #endif /* OPENRENDERBOX_CF_CGTYPES */ diff --git a/Sources/OpenRenderBoxCxx/Path/ORBPath.h b/Sources/OpenRenderBoxCxx/Path/ORBPath.h index 85269e3..30d96f3 100644 --- a/Sources/OpenRenderBoxCxx/Path/ORBPath.h +++ b/Sources/OpenRenderBoxCxx/Path/ORBPath.h @@ -7,10 +7,8 @@ #include #if OPENRENDERBOX_CF_CGTYPES - -#if ORB_TARGET_OS_DARWIN +#include #include -#endif ORB_ASSUME_NONNULL_BEGIN @@ -25,12 +23,31 @@ typedef ORB_ENUM(int32_t, ORBPathElement) { ORBPathElementCloseSubpath = 4, ORBPathElementRect = 5, ORBPathElementRoundedRect = 6, + ORBPathElementEllipse = 7, ORBPathElementFixedRoundedRectCircular = 8, ORBPathElementFixedRoundedRectContinuous = 9, + ORBPathElementUnevenRoundedRectCircular = 10, + ORBPathElementUnevenRoundedRectContinuous = 11, + ORBPathElementRelativeArc = 12, + ORBPathElementArc = 13, + ORBPathElementArcCounterclockwise = 14, + ORBPathElementArcTo = 15, + ORBPathElementEnd = 16, + ORBPathElementTransformed = 17, + ORBPathElementTrimmed = 18, + ORBPathElementStroked = 19, + ORBPathElementBoolean = 20, + ORBPathElementBooleanSeparator = 21, + ORBPathElementDilateGlyph = 22, + ORBPathElementOffset = 23, + ORBPathElementClipStroke = 24, ORBPathElementInvalid = 25, } ORB_SWIFT_NAME(ORBPath.Element); +ORB_EXPORT +const uint8_t __ORBPathElementArgumentCount[25]; + /// Defines the shape of a rounded rectangle's corners. typedef ORB_ENUM(int32_t, ORBPathRoundedCornerStyle) { /// Quarter-circle rounded rect corners. @@ -40,9 +57,23 @@ typedef ORB_ENUM(int32_t, ORBPathRoundedCornerStyle) { } ORB_SWIFT_NAME(ORBPath.RoundedCornerStyle); /// Callback type for path element enumeration -/// Returns true to stop enumeration, false to continue +/// Returns true to continue enumeration, false to stop typedef bool (*ORBPathApplyCallback)(void * info, ORBPathElement element, const CGFloat *points, const void * _Nullable userInfo); +#if ORB_TARGET_OS_DARWIN +/// Callback for mapped Core Graphics elements. Return false to stop callbacks. +typedef bool (*ORBPathApplierCallback)(void *info, const CGPathElement *element); + +typedef ORB_ENUM(int32_t, ORBPathLineElement) { + ORBPathLineElementMoveToPoint = 0, + ORBPathLineElementAddLineToPoint = 1, + ORBPathLineElementCloseSubpath = 2, +}; + +/// The point is unused for a close-subpath callback. +typedef bool (*ORBPathLineApplyCallback)(void *info, ORBPathLineElement element, const CGPoint *point); +#endif + typedef struct ORBPathCallbacks ORBPathCallbacks ORB_SWIFT_NAME(ORBPath.ApplyCallback); typedef struct ORBPathStorage * ORBPathStorageRef ORB_SWIFT_STRUCT ORB_SWIFT_NAME(ORBPath.Storage); @@ -52,6 +83,93 @@ typedef struct ORBPath { const ORBPathCallbacks * callbacks; } ORBPath; +/// A primitive shape recognized by ``ORBPathApplyShape``. +typedef ORB_ENUM(int32_t, ORBPathShapeType) { + ORBPathShapeTypeRect = 0, + ORBPathShapeTypeEllipse = 1, + ORBPathShapeTypeLine = 2, + ORBPathShapeTypeFixedRoundedRect = 3, + ORBPathShapeTypeUnevenRoundedRect = 4, + ORBPathShapeTypeNested = 5, + ORBPathShapeTypeStroked = 6, + ORBPathShapeTypeTrimmed = 7, + ORBPathShapeTypeOffset = 8, +}; + +typedef struct ORBPathRectShape { + CGAffineTransform transform; + CGRect rect; +} ORBPathRectShape; + +typedef struct ORBPathFixedRoundedRectShape { + CGAffineTransform transform; + CGRect rect; + CGSize cornerSize; + ORBPathRoundedCornerStyle style; +} ORBPathFixedRoundedRectShape; + +typedef struct ORBPathUnevenRoundedRectShape { + CGAffineTransform transform; + CGRect rect; + CGFloat topLeftRadius; + CGFloat topRightRadius; + CGFloat bottomRightRadius; + CGFloat bottomLeftRadius; + ORBPathRoundedCornerStyle style; +} ORBPathUnevenRoundedRectShape; + +typedef struct ORBPathLineShape { + CGPoint start; + CGPoint end; +} ORBPathLineShape; + +typedef struct ORBPathStrokeStyle { + CGFloat lineWidth; + int32_t lineCap; + int32_t lineJoin; + CGFloat miterLimit; + CGFloat dashPhase; + uint64_t dashCount; + const CGFloat * _Nullable dash; +} ORBPathStrokeStyle; + +typedef struct ORBPathStrokedShape { + ORBPath path; + ORBPathStrokeStyle style; +} ORBPathStrokedShape; + +typedef struct ORBPathNestedShape { + ORBPath path; + CGAffineTransform transform; +} ORBPathNestedShape; + +typedef struct ORBPathTrimmedShape { + ORBPath path; + CGFloat from; + CGFloat to; +} ORBPathTrimmedShape; + +typedef union ORBPathShape { + ORBPathRectShape rect; + ORBPathRectShape ellipse; + ORBPathLineShape line; + ORBPathFixedRoundedRectShape fixedRoundedRect; + ORBPathUnevenRoundedRectShape unevenRoundedRect; + ORBPathStrokedShape stroked; + ORBPathNestedShape nested; + ORBPathTrimmedShape trimmed; +} ORBPathShape; + +#if defined(__cplusplus) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L) +static_assert(sizeof(ORBPath) == 0x10); +static_assert(offsetof(ORBPathUnevenRoundedRectShape, rect) == 0x30); +static_assert(offsetof(ORBPathUnevenRoundedRectShape, topLeftRadius) == 0x50); +static_assert(offsetof(ORBPathUnevenRoundedRectShape, style) == 0x70); +static_assert(sizeof(ORBPathShape) == 0x78); +#endif + +typedef void (*ORBPathApplyShapeCallback)(void * info, ORBPathShapeType type, const ORBPathShape *shape); + /// Global empty path (storage = null, callbacks = &ORBPathEmptyCallbacks) ORB_EXPORT const ORBPath ORBPathEmpty ORB_SWIFT_NAME(ORBPath.empty); @@ -61,7 +179,7 @@ ORB_EXPORT const ORBPath ORBPathNull ORB_SWIFT_NAME(ORBPath.null); ORB_EXPORT -void ORBPathRetain(ORBPath path) ORB_SWIFT_NAME(ORBPath.retain(self:)); +ORBPath ORBPathRetain(ORBPath path) ORB_SWIFT_NAME(ORBPath.retain(self:)); ORB_EXPORT void ORBPathRelease(ORBPath path) ORB_SWIFT_NAME(ORBPath.release(self:)); @@ -70,7 +188,7 @@ void ORBPathRelease(ORBPath path) ORB_SWIFT_NAME(ORBPath.release(self:)); #if ORB_TARGET_OS_DARWIN ORB_EXPORT -ORBPath ORBPathMakeWithCGPath(CGPathRef cgPath) ORB_SWIFT_NAME(ORBPath.init(cgPath:)); +ORBPath ORBPathMakeWithCGPath(CGPathRef _Nullable cgPath) ORB_SWIFT_NAME(ORBPath.init(cgPath:)); ORB_EXPORT ORBPath ORBPathMakeRect(CGRect rect, const CGAffineTransform * _Nullable transform) ORB_SWIFT_NAME(ORBPath.init(rect:transform:)); @@ -82,37 +200,75 @@ ORB_EXPORT ORBPath ORBPathMakeRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, ORBPathRoundedCornerStyle style, const CGAffineTransform * _Nullable transform) ORB_SWIFT_NAME(ORBPath.init(roundedRect:cornerWidth:cornerHeight:style:transform:)); ORB_EXPORT -ORBPath ORBPathMakeUnevenRoundedRect(CGRect rect, CGFloat topLeftRadius, CGFloat bottomLeftRadius, CGFloat bottomRightRadius, CGFloat topRightRadius, ORBPathRoundedCornerStyle style, const CGAffineTransform * _Nullable transform) ORB_SWIFT_NAME(ORBPath.init(roundedRect:topLeftRadius:bottomLeftRadius:bottomRightRadius:topRightRadius:style:transform:)); +ORBPath ORBPathMakeUnevenRoundedRect(CGRect rect, CGFloat topLeftRadius, CGFloat topRightRadius, CGFloat bottomRightRadius, CGFloat bottomLeftRadius, ORBPathRoundedCornerStyle style, const CGAffineTransform * _Nullable transform) ORB_SWIFT_NAME(ORBPath.init(roundedRect:topLeftRadius:topRightRadius:bottomRightRadius:bottomLeftRadius:style:transform:)); #endif -#if ORB_TARGET_OS_DARWIN -// MARK: - Path Transformation +ORB_EXPORT +ORBPath ORBPathCopyTransformedPath(ORBPath path, const CGAffineTransform * _Nullable transform) ORB_SWIFT_NAME(ORBPath.copy(self:transform:)); + +ORB_EXPORT +ORBPath ORBPathCopyStrokedPath(ORBPath path, CGFloat lineWidth, int32_t lineCap, int32_t lineJoin, CGFloat miterLimit, CGFloat dashPhase, const CGFloat * _Nullable dash, size_t dashCount) ORB_SWIFT_NAME(ORBPath.stroked(self:lineWidth:lineCap:lineJoin:miterLimit:dashPhase:dash:dashCount:)); ORB_EXPORT -ORBPath ORBPathCopyTransformedPath(ORBPath path, const CGAffineTransform *transform) ORB_SWIFT_NAME(ORBPath.copy(self:transform:)); +ORBPath ORBPathCopyTrimmedPath(ORBPath path, CGFloat from, CGFloat to) ORB_SWIFT_NAME(ORBPath.trimmed(self:from:to:)); static ORB_INLINE bool ORBPathIsNull(ORBPath path) ORB_SWIFT_NAME(getter:ORBPath.isNull(self:)) { return path.storage == ORBPathNull.storage && path.callbacks == ORBPathNull.callbacks; } -#endif // MARK: - Path Operation ORB_EXPORT bool ORBPathIsEmpty(ORBPath path) ORB_SWIFT_NAME(getter:ORBPath.isEmpty(self:)); +ORB_EXPORT +bool ORBPathIsSingleElement(ORBPath path) ORB_SWIFT_NAME(getter:ORBPath.isSingleElement(self:)); + +ORB_EXPORT +size_t ORBPathGetBezierOrder(ORBPath path) ORB_SWIFT_NAME(getter:ORBPath.bezierOrder(self:)); + ORB_EXPORT bool ORBPathApplyElements(ORBPath path, void * info, _Nullable ORBPathApplyCallback callback) ORB_SWIFT_NAME(ORBPath.apply(self:info:callback:)); +ORB_EXPORT +bool ORBPathApplyShape(ORBPath path, bool allowLine, void * info, ORBPathApplyShapeCallback callback) ORB_SWIFT_NAME(ORBPath.applyShape(self:allowLine:info:callback:)); + ORB_EXPORT bool ORBPathEqualToPath(ORBPath lhs, ORBPath rhs) ORB_SWIFT_NAME(ORBPath.isEqual(self:to:)); // MARK: - CGPath Interoperability #if ORB_TARGET_OS_DARWIN +ORB_EXPORT +bool ORBPathApply(ORBPath path, const CGAffineTransform * _Nullable transform, void *info, ORBPathApplierCallback callback) ORB_SWIFT_NAME(ORBPath.applyCGPathElements(self:transform:info:callback:)); + +ORB_EXPORT +bool ORBPathApplyLines(ORBPath path, double tolerance, void *info, ORBPathLineApplyCallback callback) ORB_SWIFT_NAME(ORBPath.applyLines(self:tolerance:info:callback:)); + +ORB_EXPORT +double ORBPathGetLength(ORBPath path, double tolerance, const CGAffineTransform * _Nullable transform) ORB_SWIFT_NAME(ORBPath.length(self:tolerance:transform:)); + +ORB_EXPORT +bool ORBPathCanInterpolate(ORBPath path, ORBPath other) ORB_SWIFT_NAME(ORBPath.canInterpolate(self:to:)); + +ORB_EXPORT +ORBPath ORBPathCopyInterpolatedPath(ORBPath path, ORBPath other, float fraction) ORB_SWIFT_NAME(ORBPath.interpolated(self:to:fraction:)); + +ORB_EXPORT +CFDataRef ORBPathCopyData(ORBPath path) ORB_SWIFT_NAME(getter:ORBPath.data(self:)); + +ORB_EXPORT +ORBPath ORBPathMakeWithData(CFDataRef data) ORB_SWIFT_NAME(ORBPath.init(data:)); + +ORB_EXPORT +CFStringRef ORBPathCopyDebugDescription(ORBPath path) ORB_SWIFT_NAME(getter:ORBPath.debugDescription(self:)); + ORB_EXPORT CGPathRef ORBPathCopyCGPath(ORBPath path) ORB_SWIFT_NAME(getter:ORBPath.cgPath(self:)); + +ORB_EXPORT +CGRect ORBPathGetBoundingRect(ORBPath path) ORB_SWIFT_NAME(getter:ORBPath.boundingRect(self:)); #endif // MARK: - Point Containment @@ -121,7 +277,7 @@ ORB_EXPORT bool ORBPathContainsPoint(ORBPath path, CGPoint point, bool eoFill) ORB_SWIFT_NAME(ORBPath.contains(self:point:eoFill:)); ORB_EXPORT -bool ORBPathContainsPoints(ORBPath path, uint64_t count, const CGPoint *points, bool eoFill, const CGAffineTransform * _Nullable transform) ORB_SWIFT_NAME(ORBPath.containsPoints(self:count:points:eoFill:transform:)); +uint64_t ORBPathContainsPoints(ORBPath path, uint64_t count, const CGPoint *points, bool eoFill, const CGAffineTransform * _Nullable transform) ORB_SWIFT_NAME(ORBPath.containsPoints(self:count:points:eoFill:transform:)); #endif ORB_EXTERN_C_END diff --git a/Sources/OpenRenderBoxCxx/Path/ORBPathCallbacks.cpp b/Sources/OpenRenderBoxCxx/Path/ORBPathCallbacks.cpp index 360b503..c1691cc 100644 --- a/Sources/OpenRenderBoxCxx/Path/ORBPathCallbacks.cpp +++ b/Sources/OpenRenderBoxCxx/Path/ORBPathCallbacks.cpp @@ -18,7 +18,7 @@ typedef void (^CGPathApplyBlock2)(const CGPathElement * element, bool *stop); CG_EXTERN void CGPathApplyWithBlock2(CGPathRef path, CGPathApplyBlock2 CF_NOESCAPE block); namespace { - uint32_t cgpath_bezier_order(CGPathRef cgPath) { + size_t cgpath_bezier_order(CGPathRef cgPath) { size_t pointsCount = CGPathGetNumberOfPoints(cgPath); size_t elementsCount = CGPathGetNumberOfElements(cgPath); if (pointsCount > elementsCount) { return 3; } @@ -62,7 +62,7 @@ const ORBPathCallbacks ORBPathCGPathCallbacks = { .isSingleElement = +[](const void *object) -> bool { return false; }, - .bezierOrder = +[](const void *object) -> uint32_t { + .bezierOrder = +[](const void *object) -> size_t { return cgpath_bezier_order(static_cast(object)); }, .boundingRect = +[](const void *object) -> CGRect { diff --git a/Sources/OpenRenderBoxCxx/Path/ORBPathCallbacks.h b/Sources/OpenRenderBoxCxx/Path/ORBPathCallbacks.h index 63efd77..39c5d8a 100644 --- a/Sources/OpenRenderBoxCxx/Path/ORBPathCallbacks.h +++ b/Sources/OpenRenderBoxCxx/Path/ORBPathCallbacks.h @@ -39,14 +39,14 @@ typedef struct ORB_SWIFT_NAME(ORBPath.Callbacks) ORBPathCallbacks { bool (* _Nullable isEqual)(const void *object, const void *otherObject); bool (* _Nullable isEmpty)(const void *object); bool (* _Nullable isSingleElement)(const void *object); - uint32_t (* _Nullable bezierOrder)(const void *object); + size_t (* _Nullable bezierOrder)(const void *object); CGRect (* _Nullable boundingRect)(const void *object); #if ORB_TARGET_OS_DARWIN CGPathRef _Nullable (* _Nullable cgPath)(const void *object); #else void * _Nullable (* _Nullable cgPath)(const void *object); #endif - const struct ORBPathCallbacks * _Nullable (* _Nullable next)(const void *object); + ORBPath (* _Nullable next)(const void *object); } ORBPathCallbacks; // static_assert without message requires C23 or C++17 @@ -74,14 +74,14 @@ typedef struct ORB_SWIFT_NAME(ORBPath.CallbacksExtended) ORBPathCallbacksExtende bool (* _Nullable isEqual)(const void *object, const void *otherObject, const struct ORBPathCallbacksExtended *extended); bool (* _Nullable isEmpty)(const void *object, const struct ORBPathCallbacksExtended *extended); bool (* _Nullable isSingleElement)(const void *object, const struct ORBPathCallbacksExtended *extended); - uint32_t (* _Nullable bezierOrder)(const void *object, const struct ORBPathCallbacksExtended *extended); + size_t (* _Nullable bezierOrder)(const void *object, const struct ORBPathCallbacksExtended *extended); CGRect (* _Nullable boundingRect)(const void *object, const struct ORBPathCallbacksExtended *extended); #if ORB_TARGET_OS_DARWIN CGPathRef _Nullable (* _Nullable cgPath)(const void *object, const struct ORBPathCallbacksExtended *extended); #else void * _Nullable (* _Nullable cgPath)(const void *object, const struct ORBPathCallbacksExtended *extended); #endif - const struct ORBPathCallbacksExtended * _Nullable (* _Nullable next)(const void *object, const struct ORBPathCallbacksExtended *extended); + ORBPath (* _Nullable next)(const void *object, const struct ORBPathCallbacksExtended *extended); } ORBPathCallbacksExtended; // static_assert without message requires C23 or C++17 diff --git a/Sources/OpenRenderBoxCxx/Path/ORBPathStorage.cpp b/Sources/OpenRenderBoxCxx/Path/ORBPathStorage.cpp index 926a30c..1512ed4 100644 --- a/Sources/OpenRenderBoxCxx/Path/ORBPathStorage.cpp +++ b/Sources/OpenRenderBoxCxx/Path/ORBPathStorage.cpp @@ -7,25 +7,24 @@ #include #include "Storage.hpp" #include +#include using namespace ORB; namespace ORB { namespace Path { -namespace { bool append_element_callback(void * info, ORBPathElement element, const CGFloat *points, const void * _Nullable userInfo) { reinterpret_cast(info)->append_element(element, points, userInfo); return true; } -} /* anonymous namespace */ } /* Path */ } /* ORB */ -void ORBPathStorageInit(ORBPathStorageRef dst, uint32_t capacity, ORBPathStorageRef source) { +void ORBPathStorageInit(ORBPathStorageRef dst, size_t capacity, ORBPathStorageRef source) { if (source != nullptr) { - dst->storage = ORB::Path::Storage(capacity, source->storage); + new (dst) ORBPathStorage{ORB::Path::Storage(capacity, source->storage)}; } else { - dst->storage = ORB::Path::Storage(capacity); + new (dst) ORBPathStorage{ORB::Path::Storage(capacity)}; } } @@ -37,11 +36,19 @@ void ORBPathStorageClear(ORBPathStorageRef storage) { storage->storage.clear(); } -bool ORBPathStorageAppendElement(ORBPathStorageRef storage, ORBPathElement element, const CGFloat * points, const void * userInfo) { - return storage->storage.append_element(element, points, userInfo); +void ORBPathStorageAppendElement(ORBPathStorageRef storage, ORBPathElement element, const CGFloat * points, const void * userInfo) { + storage->storage.append_element(element, points, userInfo); } bool ORBPathStorageAppendPath(ORBPathStorageRef storage, ORBPath path) { + if (path.storage == storage) { + ORB::Path::Storage snapshot(sizeof(ORB::Path::Storage)); + if (!ORBPathApplyElements(path, &snapshot, ORB::Path::append_element_callback)) { + return false; + } + storage->storage.append_storage(snapshot); + return true; + } return ORBPathApplyElements(path, storage, ORB::Path::append_element_callback); } @@ -61,18 +68,17 @@ bool ORBPathStorageIsSingleElement(ORBPathStorageRef storage) { return storage->storage.flags().isSingleElement(); } -uint32_t ORBPathStorageGetBezierOrder(ORBPathStorageRef storage) { +size_t ORBPathStorageGetBezierOrder(ORBPathStorageRef storage) { return storage->storage.flags().bezierOrder(); } #if ORB_TARGET_OS_DARWIN CGRect ORBPathStorageGetBoundingRect(ORBPathStorageRef storage) { - precondition_failure("TODO"); + return storage->storage.bounding_rect(); } CGPathRef ORBPathStorageGetCGPath(ORBPathStorageRef storage) { - // FIXME return storage->storage.cgpath(); } #endif diff --git a/Sources/OpenRenderBoxCxx/Path/ORBPathStorage.h b/Sources/OpenRenderBoxCxx/Path/ORBPathStorage.h index 0dce577..ba91927 100644 --- a/Sources/OpenRenderBoxCxx/Path/ORBPathStorage.h +++ b/Sources/OpenRenderBoxCxx/Path/ORBPathStorage.h @@ -21,7 +21,7 @@ ORB_ASSUME_NONNULL_BEGIN ORB_EXTERN_C_BEGIN ORB_EXPORT -void ORBPathStorageInit(ORBPathStorageRef dst, uint32_t capacity, ORBPathStorageRef _Nullable source) ORB_SWIFT_NAME(ORBPathStorageRef.initialize(self:capacity:source:)); +void ORBPathStorageInit(ORBPathStorageRef dst, size_t capacity, ORBPathStorageRef _Nullable source) ORB_SWIFT_NAME(ORBPathStorageRef.initialize(self:capacity:source:)); ORB_EXPORT void ORBPathStorageDestroy(ORBPathStorageRef storage) ORB_SWIFT_NAME(ORBPathStorageRef.destroy(self:)); @@ -30,7 +30,7 @@ ORB_EXPORT void ORBPathStorageClear(ORBPathStorageRef storage) ORB_SWIFT_NAME(ORBPathStorageRef.clear(self:)); ORB_EXPORT -bool ORBPathStorageAppendElement(ORBPathStorageRef storage, ORBPathElement element, CGFloat const * points, const void * _Nullable userInfo) ORB_SWIFT_NAME(ORBPathStorageRef.append(self:element:points:userInfo:)); +void ORBPathStorageAppendElement(ORBPathStorageRef storage, ORBPathElement element, CGFloat const * points, const void * _Nullable userInfo) ORB_SWIFT_NAME(ORBPathStorageRef.append(self:element:points:userInfo:)); ORB_EXPORT bool ORBPathStorageAppendPath(ORBPathStorageRef, ORBPath) ORB_SWIFT_NAME(ORBPathStorageRef.append(self:path:)); @@ -48,7 +48,7 @@ ORB_EXPORT bool ORBPathStorageIsSingleElement(ORBPathStorageRef storage) ORB_SWIFT_NAME(getter:ORBPathStorageRef.isSingleElement(self:)); ORB_EXPORT -uint32_t ORBPathStorageGetBezierOrder(ORBPathStorageRef storage) ORB_SWIFT_NAME(getter:ORBPathStorageRef.bezierOrder(self:)); +size_t ORBPathStorageGetBezierOrder(ORBPathStorageRef storage) ORB_SWIFT_NAME(getter:ORBPathStorageRef.bezierOrder(self:)); #if ORB_TARGET_OS_DARWIN ORB_EXPORT diff --git a/Sources/OpenRenderBoxCxx/Path/PathClipStroke.cpp b/Sources/OpenRenderBoxCxx/Path/PathClipStroke.cpp new file mode 100644 index 0000000..2e21189 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/PathClipStroke.cpp @@ -0,0 +1,599 @@ +// +// PathClipStroke.cpp +// OpenRenderBox + +#include "PathClipStroke.hpp" + +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN + +#include "ClipStrokeKeyframes.hpp" +#include "PathGeometry.hpp" +#include +#include +#include +#include + +namespace ORB::Path { +namespace { + +using Geometry::Cubic; +using Geometry::curve_measure; +using Geometry::distance; +using Geometry::mix; +using ClipStroke::Keyframe; + +CGPoint add(CGPoint a, CGPoint b) { return {a.x + b.x, a.y + b.y}; } +CGPoint subtract(CGPoint a, CGPoint b) { return {a.x - b.x, a.y - b.y}; } +CGPoint multiply(CGPoint a, double b) { return {a.x * b, a.y * b}; } +CGPoint scale_add(CGPoint a, CGPoint b, double c) { return {std::fma(b.x, c, a.x), std::fma(b.y, c, a.y)}; } +double dot(CGPoint a, CGPoint b) { return std::fma(a.x, b.x, a.y * b.y); } +CGPoint unit(CGPoint a) { return multiply(a, 1 / std::sqrt(a.x * a.x + a.y * a.y)); } +bool equal(CGPoint a, CGPoint b) { return a.x == b.x && a.y == b.y; } +bool finite(CGPoint p) { return std::isfinite(p.x) && std::isfinite(p.y); } +CGPoint apply_transform(CGPoint p, const CGAffineTransform *transform) { + if (!transform) return p; + const auto &t = *transform; + return {std::fma(t.c, p.y, std::fma(t.a, p.x, t.tx)), + std::fma(t.d, p.y, std::fma(t.b, p.x, t.ty))}; +} + +struct Segment { + unsigned degree = 1; + CGPoint points[4] = {}; + double length = 0; + + CGPoint end() const { return points[degree]; } + + Cubic cubic() const { + if (degree == 3) return {{points[0], points[1], points[2], points[3]}}; + return {{points[0], mix(points[0], points[1], 2.0 / 3), + mix(points[2], points[1], 2.0 / 3), points[2]}}; + } + + double measure() const { + if (degree == 1) return distance(points[0], points[1]); + if (degree == 2) return Geometry::quadratic_measure({{points[0], points[1], points[2]}}, 0, false); + return curve_measure(cubic(), 0, false); + } + + double index(double value) const { + if (degree == 1) return std::fmin(std::fmax(value / measure(), 0), 1); + if (degree == 2) return Geometry::quadratic_measure({{points[0], points[1], points[2]}}, value, true); + return curve_measure(cubic(), value, true); + } + + void split(double t, Segment &left, Segment &right) const { + left.degree = right.degree = degree; + CGPoint work[4]; + std::copy_n(points, degree + 1, work); + left.points[0] = work[0]; + right.points[degree] = work[degree]; + for (unsigned level = 1; level <= degree; ++level) { + for (unsigned i = 0; i <= degree - level; ++i) work[i] = mix(work[i], work[i + 1], t); + left.points[level] = work[0]; + right.points[degree - level] = work[degree - level]; + } + left.length = left.measure(); + right.length = right.measure(); + } + + CGPoint direction(bool atEnd) const { + if (atEnd) { + for (unsigned i = degree; i > 0; --i) { + CGPoint result = subtract(points[degree], points[i - 1]); + if (dot(result, result) > 1e-8) return result; + } + } else { + for (unsigned i = 1; i <= degree; ++i) { + CGPoint result = subtract(points[i], points[0]); + if (dot(result, result) > 1e-8) return result; + } + } + return subtract(points[degree], points[0]); + } + + void replay(CGMutablePathRef path, const CGAffineTransform *transform = nullptr) const { + CGPoint p[4]; + for (unsigned i = 1; i <= degree; ++i) p[i] = apply_transform(points[i], transform); + if (degree == 1) CGPathAddLineToPoint(path, nullptr, p[1].x, p[1].y); + else if (degree == 2) CGPathAddQuadCurveToPoint(path, nullptr, p[1].x, p[1].y, p[2].x, p[2].y); + else CGPathAddCurveToPoint(path, nullptr, p[1].x, p[1].y, p[2].x, p[2].y, p[3].x, p[3].y); + } +}; + +struct Subpath { + CGPoint start = CGPointZero; + std::vector segments; + double length = 0; + + CGPoint end() const { return segments.empty() ? start : segments.back().end(); } + + void append_line(CGPoint point) { + Segment segment{1, {end(), point}}; + segment.length = segment.measure(); + length += segment.length; + segments.push_back(segment); + } + + double append(Segment segment, double suppliedLength = INFINITY) { + bool nonzero = false; + for (unsigned i = 0; i < segment.degree; ++i) { + CGPoint edge = subtract(segment.points[i + 1], segment.points[i]); + if (dot(edge, edge) > 1e-8) nonzero = true; + } + if (!nonzero) return length; + if (segments.empty()) start = segment.points[0]; + else if (!equal(end(), segment.points[0])) append_line(segment.points[0]); + segment.length = std::isfinite(suppliedLength) ? suppliedLength : segment.measure(); + if (!std::isfinite(segment.length)) return length; + double begin = length; + length += segment.length; + segments.push_back(segment); + return begin; + } + + void close() { + if (!segments.empty() && !equal(end(), start)) append_line(start); + } + + void replay(CGMutablePathRef path, bool close, const CGAffineTransform *transform = nullptr) const { + if (segments.empty()) return; + CGPoint point = apply_transform(start, transform); + if (close) CGPathMoveToPoint(path, nullptr, point.x, point.y); + else CGPathAddLineToPoint(path, nullptr, point.x, point.y); + for (const auto &segment : segments) segment.replay(path, transform); + if (close) CGPathCloseSubpath(path); + } + + void append_range(const Subpath &source, double from, double to, ClipStroke::DistanceMap *map = nullptr) { + double offset = 0; + for (const auto &segment : source.segments) { + double end = offset + segment.length; + auto appendPiece = [&](double begin, double finish) { + begin = std::fmin(std::fmax(begin, offset), end); + finish = std::fmin(std::fmax(finish, offset), end); + if (finish - begin < 0.0001) return; + double t0 = begin - offset < 0.0001 ? 0 : segment.index(begin - offset); + double t1 = finish - offset > segment.length - 0.0001 ? 1 : segment.index(finish - offset); + double destination; + double sourceLength = finish - begin; + if (t0 <= 0.0001 && t1 >= 0.9999) { + destination = append(segment, segment.length); + sourceLength = segment.length; + } else { + Segment piece = segment, left, right; + bool splitEnd = t1 < 0.9999; + if (t0 > 0.0001) { + piece.split(t0, left, right); + piece = right; + if (splitEnd) t1 = (t1 - t0) / (1 - t0); + } + if (splitEnd) { + piece.split(t1, left, right); + piece = left; + } + destination = append(piece); + } + if (map) map->append(begin, destination, sourceLength); + }; + if (to <= from) { + appendPiece(offset, to); + appendPiece(from, end); + } else appendPiece(from, to); + offset = end; + } + } + + void append_logical_range(const Subpath &source, double from, double to) { + if (from > to) { + append_range(source, from, source.length); + append_range(source, 0, to); + } else append_range(source, from, to); + } +}; + +struct Clipper { + CGMutablePathRef output; + const ClipStrokeInfo &info; + float progress; + const Subpath &subpath; + const Subpath *customCap; + double rate; + ClipStroke::Cursor cursor; + double consumed = 0; + float value; + bool clipped; + bool emitted = false; + CGPoint lastPoint = CGPointZero; + CGPoint lastDirection = CGPointZero; + double lastSpan = 0; + CGPoint firstPoint = {INFINITY, INFINITY}; + CGPoint firstDirection = CGPointZero; + double firstSpan = 0; + // A contour can emit its first segment before cap() records its start. + float firstValue; + + Clipper(CGMutablePathRef output, const ClipStrokeInfo &info, float progress, + const Subpath &subpath, const Subpath *cap, + const std::vector &keys, double rate) + : output(output), info(info), progress(progress), subpath(subpath), customCap(cap), rate(rate), + cursor(keys, subpath.length), value(cursor.initialValue()), + clipped(ClipStroke::valueClipped(info.flags, progress, value)), firstValue(value) {} + + double span() const { + if (cursor.next.tag & 1) return INFINITY; + double amount = std::abs(cursor.next.distance - cursor.previous.distance); + return std::fmin(amount, subpath.length - amount); + } + + void emit(const Segment &segment) { + bool nonzero = false; + for (unsigned i = 0; i < segment.degree; ++i) { + CGPoint edge = subtract(segment.points[i + 1], segment.points[i]); + if (dot(edge, edge) > 1e-8) nonzero = true; + } + if (!nonzero) return; + if (!emitted) { + firstPoint = segment.points[0]; + firstDirection = multiply(segment.direction(false), -1); + CGPathMoveToPoint(output, nullptr, firstPoint.x, firstPoint.y); + emitted = true; + } + segment.replay(output); + lastPoint = segment.end(); + lastDirection = segment.direction(true); + lastSpan = span(); + } + + void line(CGPoint point) { CGPathAddLineToPoint(output, nullptr, point.x, point.y); } + void cubic(CGPoint a, CGPoint b, CGPoint c) { + CGPathAddCurveToPoint(output, nullptr, a.x, a.y, b.x, b.y, c.x, c.y); + } + + void cap(CGPoint point, CGPoint direction, double nextSpan) { + if (!emitted) { + firstPoint = point; + firstDirection = direction; + firstSpan = nextSpan; + firstValue = value; + return; + } + if (info.flags & 8) { + line(point); + lastPoint = point; + lastDirection = multiply(direction, -1); + lastSpan = nextSpan; + return; + } + CGPoint previousDirection = unit(lastDirection); + CGPoint nextDirection = unit(direction); + CGPoint axis = lastSpan > nextSpan ? previousDirection : nextDirection; + double projection = dot(axis, subtract(point, lastPoint)); + CGPoint from = projection > 0 ? scale_add(lastPoint, axis, projection) : lastPoint; + CGPoint to = projection > 0 ? point : scale_add(point, axis, -projection); + if (!(info.flags & 2)) { + float overshoot = 0; + if (!(info.flags & 1) && value >= 1) overshoot = progress - value; + else if ((info.flags & 1) && value <= 0) overshoot = value - progress; + if (overshoot > 0) { + CGPoint shift = add(previousDirection, nextDirection); + double amount = rate * double(overshoot) / distance(CGPointZero, shift); + from = scale_add(from, shift, amount); + to = scale_add(to, shift, amount); + } + } + if (!equal(from, lastPoint)) line(from); + if (customCap && !customCap->segments.empty()) { + CGPoint source = subtract(customCap->end(), customCap->start); + CGPoint target = subtract(to, from); + double sourceLength = std::sqrt(source.x * source.x + source.y * source.y); + double targetLength = std::sqrt(target.x * target.x + target.y * target.y); + source = {source.x / sourceLength, source.y / sourceLength}; + target = {target.x / targetLength, target.y / targetLength}; + double scale = targetLength / sourceLength; + double a = (source.x * target.x + source.y * target.y) * scale; + double b = (-source.y * target.x + source.x * target.y) * scale; + CGAffineTransform transform = CGAffineTransformMake(a, b, -b, a, from.x, from.y); + transform = CGAffineTransformTranslate(transform, -customCap->start.x, -customCap->start.y); + customCap->replay(output, false, &transform); + } else { + CGPoint gap = subtract(to, from); + CGPoint normal = {-axis.y, axis.x}; + if (dot(CGPoint{-gap.y, gap.x}, axis) < 0) normal = multiply(normal, -1); + double radius = distance(from, to) * 0.5; + CGPoint lateral = multiply(normal, radius); + CGPoint forward = multiply(axis, radius); + CGPoint center = scale_add(from, normal, -radius); + switch (info.cap) { + case 1: { + constexpr double k = 0.5522847498307936; + CGPoint middle = add(center, forward); + cubic(scale_add(from, forward, k), scale_add(middle, lateral, k), middle); + CGPoint finish = subtract(center, lateral); + cubic(scale_add(middle, lateral, -k), scale_add(finish, forward, k), finish); + break; + } + case 2: + line(add(from, forward)); + line(add(subtract(center, lateral), forward)); + line(subtract(center, lateral)); + break; + case 3: case 5: + line(add(center, forward)); + line(subtract(center, lateral)); + break; + case 4: case 6: + line(add(from, forward)); + line(center); + line(add(subtract(center, lateral), forward)); + line(subtract(center, lateral)); + break; + default: + line(to); + } + } + if (!equal(to, point)) line(point); + lastPoint = point; + lastDirection = multiply(direction, -1); + lastSpan = nextSpan; + } + + void clip(const Segment &segment, double length, float nextValue) { + bool nextClipped = ClipStroke::valueClipped(info.flags, progress, nextValue); + if (clipped == nextClipped) { + if (!clipped) emit(segment); + } else { + double fraction = 0; + if (value != nextValue) fraction = double((std::fmin(std::fmax(progress, 0.0f), 1.0f) - value) / (nextValue - value)); + double t = segment.index(fraction * length); + if (t > 0.9999) { + if (!clipped) emit(segment); + } else { + Segment left, right; + segment.split(t, left, right); + if (clipped) { + cap(right.points[0], multiply(right.direction(false), -1), span()); + emit(right); + } else if (t > 0.0001) emit(left); + clipped = nextClipped; + } + } + value = nextValue; + } + + bool undershoot() { + float minimum = std::numeric_limits::infinity(); + double from = 0, to = 0; + for (const auto &key : cursor.keys) { + if (key.value < minimum) { + minimum = key.value; + from = key.distance; + to = INFINITY; + } else if (key.value == minimum) { + to = key.distance; + } + } + for (const auto &key : cursor.keys) { + if (std::isfinite(key.value) && key.value != minimum && + key.distance >= from && key.distance < to) { + std::swap(from, to); + break; + } + } + + Subpath selected; + selected.append_logical_range(subpath, from, to); + if (selected.segments.empty()) return false; + + CGPoint firstTangent = CGPointZero; + CGPoint lastTangent = CGPointZero; + for (const auto &segment : selected.segments) { + bool first = equal(firstTangent, CGPointZero); + CGPoint tangent; + if (segment.degree == 2) { + CGPoint edge = subtract(segment.points[1], segment.points[0]); + CGPoint linear = add(edge, edge); + CGPoint quadratic = subtract(segment.points[2], add(segment.points[1], edge)); + tangent = scale_add(linear, quadratic, first ? 0 : 2); + } else if (segment.degree == 3) { + unsigned endpoint = first ? 0 : 3; + unsigned adjacent = first ? 1 : 2; + auto difference = [&](unsigned point) { + return first ? subtract(segment.points[point], segment.points[endpoint]) + : subtract(segment.points[endpoint], segment.points[point]); + }; + tangent = difference(adjacent); + if (tangent.x * tangent.x + tangent.y * tangent.y <= 1e-8) { + tangent = difference(first ? 2 : 1); + if (tangent.x * tangent.x + tangent.y * tangent.y <= 1e-8) + tangent = difference(first ? 3 : 0); + } + } else tangent = subtract(segment.points[1], segment.points[0]); + if (first) firstTangent = tangent; + else lastTangent = tangent; + } + // A single segment only sets the first tangent in the source consumer. + if (equal(firstTangent, CGPointZero) || equal(lastTangent, CGPointZero)) return false; + float amount = minimum - progress; + if (info.flags & 1) amount = -amount; + if (!(amount > 0)) return false; + + CGPoint axis = scale_add(multiply(unit(lastTangent), -1), firstTangent, + 1 / distance(CGPointZero, firstTangent)); + double offset = (rate * double(amount)) / distance(CGPointZero, axis); + CGPoint translation = multiply(axis, offset); + CGPoint shiftedStart = scale_add(selected.start, axis, offset); + CGPoint shiftedEnd = add(selected.end(), translation); + CGPathMoveToPoint(output, nullptr, shiftedStart.x, shiftedStart.y); + emitted = true; + CGAffineTransform transform = CGAffineTransformMakeTranslation(translation.x, translation.y); + selected.replay(output, false, &transform); + lastPoint = shiftedEnd; + lastDirection = lastTangent; + lastSpan = 0; + value = minimum; + cap(shiftedStart, multiply(firstTangent, -1), 0); + CGPathCloseSubpath(output); + emitted = false; + return true; + } + + void run() { + bool split = cursor.next.distance > cursor.previous.distance; + if (split && (info.flags & 6) == 4 && + ((info.flags & 1) ? progress > 1 : progress < 0) && undershoot()) return; + for (const auto &input : subpath.segments) { + if (!split) { + emit(input); + continue; + } + Segment remaining = input; + double begin = consumed, end = begin + input.length, length = input.length; + while (cursor.next.distance < end) { + double t = remaining.index(cursor.next.distance - begin); + if (t > 0.0001) { + Segment left, right; + remaining.split(t, left, right); + clip(left, left.length, cursor.next.value); + length -= left.length; + remaining = right; + begin = cursor.next.distance; + } + double previous = cursor.next.distance; + cursor.advance(previous); + if (!(cursor.next.distance > previous)) break; + } + clip(remaining, length, cursor.valueAt(end)); + consumed = end; + } + if (emitted) { + if (clipped || ClipStroke::valueClipped(info.flags, progress, firstValue)) { + clipped = true; + if (finite(firstPoint)) { + value = firstValue; + cap(firstPoint, firstDirection, firstSpan); + } + } + CGPathCloseSubpath(output); + } + } +}; + +struct ClipInput { + CGMutablePathRef output; + const ClipStrokeInfo &info; + float progress; + Subpath subpath; + size_t subpathIndex = 0; + size_t keyIndex = 0; + CGPoint current = CGPointZero; + + void run(const Subpath &path, const Subpath *cap, const std::vector &inputKeys) { + std::vector keys; + double rate = 0; + if (ClipStroke::makeKeyframes(inputKeys.data(), inputKeys.size(), path.length, progress, keys, rate)) { + Clipper(output, info, progress, path, cap, keys, rate).run(); + } else path.replay(output, true); + } + + void process(std::vector keys) { + Subpath customCap; + size_t selected = keys.size(); + float selectedValue = INFINITY; + float sign = info.flags & 1 ? 1 : -1; + for (size_t i = 0; i < keys.size(); ++i) { + float value = keys[i].value * sign; + if ((keys[i].tag & 2) && value < selectedValue) { + selected = i; + selectedValue = value; + } + } + const Subpath *cap = nullptr; + if (selected < keys.size() && std::isfinite(selectedValue)) { + float first = std::fmin(keys[selected].start, keys[selected].end); + float last = std::fmax(keys[selected].start, keys[selected].end); + bool reverse = false; + for (size_t i = 0; i < keys.size(); ++i) { + if (i == selected) continue; + for (float position : {keys[i].start, keys[i].end}) { + if (first <= position && position < last) reverse = true; + } + } + if (reverse) std::swap(first, last); + customCap.append_logical_range(subpath, + ClipStroke::positionDistance(first, subpath.length), + ClipStroke::positionDistance(last, subpath.length)); + cap = &customCap; + } + for (size_t i = 0; i < keys.size(); ++i) { + ClipStroke::TaggedSplit split; + if (!ClipStroke::selectTaggedSplit(keys, i, info.flags, progress, split)) continue; + Subpath first, second; + ClipStroke::DistanceMap firstMap, secondMap; + first.append_range(subpath, + ClipStroke::positionDistance(split.firstStart, subpath.length), + ClipStroke::positionDistance(split.firstEnd, subpath.length), &firstMap); + first.close(); + auto firstKeys = ClipStroke::remapKeyframes(keys, i, subpath.length, first.length, firstMap); + run(first, cap, firstKeys); + second.append_range(subpath, + ClipStroke::positionDistance(split.secondStart, subpath.length), + ClipStroke::positionDistance(split.secondEnd, subpath.length), &secondMap); + second.close(); + keys = ClipStroke::remapKeyframes(keys, i, subpath.length, second.length, secondMap); + subpath = std::move(second); + i = 0; + } + run(subpath, cap, keys); + } + + void close() { + if (!subpath.segments.empty()) { + subpath.close(); + while (keyIndex < info.count && std::floor(info.keys[keyIndex].start) < float(subpathIndex)) ++keyIndex; + size_t first = keyIndex; + while (keyIndex < info.count && std::floor(info.keys[keyIndex].start) <= float(subpathIndex)) ++keyIndex; + std::vector keys; + if (first < keyIndex) keys.assign(info.keys + first, info.keys + keyIndex); + process(std::move(keys)); + subpath = {}; + } + ++subpathIndex; + } + + void append(const CGPathElement &element) { + if (element.type == kCGPathElementMoveToPoint) { + if (!subpath.segments.empty()) close(); + current = subpath.start = element.points[0]; + } else if (element.type == kCGPathElementCloseSubpath) { + current = subpath.start; + close(); + } else { + unsigned degree = unsigned(element.type); + Segment segment; + segment.degree = degree; + segment.points[0] = current; + std::copy_n(element.points, degree, segment.points + 1); + current = segment.end(); + subpath.append(segment); + } + } +}; + +} // namespace + +CGPathRef copy_clip_stroke_path(CGPathRef path, float progress, const ClipStrokeInfo &info) { + progress = ClipStroke::normalizeProgress(info.flags, progress); + if (progress == float(1 & ~info.flags)) return CGPathCreateCopy(path); + CGMutablePathRef result = CGPathCreateMutable(); + ClipInput input{result, info, progress, {}}; + CGPathApply(path, &input, +[](void *context, const CGPathElement *element) { + static_cast(context)->append(*element); + }); + if (!input.subpath.segments.empty()) input.close(); + return result; +} + +} // namespace ORB::Path + +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/PathClipStroke.hpp b/Sources/OpenRenderBoxCxx/Path/PathClipStroke.hpp new file mode 100644 index 0000000..30360d8 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/PathClipStroke.hpp @@ -0,0 +1,35 @@ +// +// PathClipStroke.hpp +// OpenRenderBox + +#pragma once + +#include + +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN + +#include +#include +#include + +namespace ORB::Path { + +struct ClipStrokeKeyframe { + int32_t tag; + float value; + float start; + float end; +}; + +struct ClipStrokeInfo { + uint32_t flags; + int32_t cap; + size_t count; + const ClipStrokeKeyframe *keys; +}; + +CGPathRef copy_clip_stroke_path(CGPathRef path, float progress, const ClipStrokeInfo &info); + +} // namespace ORB::Path + +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/PathData.cpp b/Sources/OpenRenderBoxCxx/Path/PathData.cpp new file mode 100644 index 0000000..e23bbf9 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/PathData.cpp @@ -0,0 +1,433 @@ +// +// PathData.cpp +// OpenRenderBox + +#include + +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN + +#include +#include "PathClipStroke.hpp" +#include "PathStorageObject.hpp" +#include "Storage.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; +using ORB::Path::Storage; +using ORB::Path::ClipStrokeInfo; +using ORB::Path::ClipStrokeKeyframe; + +struct Encoder { + Bytes bytes; + + void varint(uint64_t value) { + do { + uint8_t byte = value & 0x7f; + value >>= 7; + bytes.push_back(byte | (value ? 0x80 : 0)); + } while (value); + } + + void integer(unsigned field, uint64_t value) { + if (!value) return; + varint(uint64_t(field) << 3); + varint(value); + } + + template void scalar(T value) { + const auto *start = reinterpret_cast(&value); + bytes.insert(bytes.end(), start, start + sizeof(T)); + } + + void real(unsigned field, float value, float defaultValue = 0) { + if (value == defaultValue) return; + varint((uint64_t(field) << 3) | 5); + scalar(value); + } + + void data(unsigned field, const Bytes &value) { + varint((uint64_t(field) << 3) | 2); + varint(value.size()); + bytes.insert(bytes.end(), value.begin(), value.end()); + } +}; + +struct Field { + unsigned number = 0; + unsigned wire = 0; + uint64_t integer = 0; + const uint8_t *data = nullptr; + size_t size = 0; + + template T scalar() const { + T value; + std::memcpy(&value, data, sizeof(T)); + return value; + } +}; + +struct Decoder { + const uint8_t *bytes; + size_t size; + size_t offset = 0; + bool valid = true; + + bool varint(uint64_t &result) { + result = 0; + for (unsigned shift = 0; shift < 70; shift += 7) { + if (offset == size) return valid = false; + uint8_t byte = bytes[offset++]; + if (shift == 63 && byte > 1) return valid = false; + result |= uint64_t(byte & 0x7f) << shift; + if (!(byte & 0x80)) return true; + } + return valid = false; + } + + bool next(Field &field) { + if (!valid || offset == size) return false; + uint64_t key; + if (!varint(key) || key >> 3 == 0 || key >> 3 > 0x1fffffff) return valid = false; + field = {}; + field.number = unsigned(key >> 3); + field.wire = key & 7; + if (field.wire == 0) return varint(field.integer); + uint64_t count; + if (field.wire == 1) count = 8; + else if (field.wire == 5) count = 4; + else if (field.wire == 2) { + if (!varint(count)) return false; + } else return valid = false; + if (count > size - offset) return valid = false; + field.data = bytes + offset; + field.size = size_t(count); + offset += field.size; + return true; + } +}; + +struct Metadata { + unsigned kind = 0; + CGAffineTransform affine = CGAffineTransformIdentity; + Storage::StrokeInfo stroke = {}; + uint8_t boolean[2] = {}; + ClipStrokeInfo clip = {0, 1, 0, nullptr}; + std::vector dash; + std::vector keys; + + const void *pointer() { + switch (kind) { + case 1: return &affine; + case 2: + stroke.count = dash.size(); + stroke.values = dash.data(); + return &stroke; + case 3: return boolean; + case 4: + clip.count = keys.size(); + clip.keys = keys.data(); + return &clip; + default: return nullptr; + } + } +}; + +template bool read_reals(const Field &field, F append) { + if (field.wire != 2 && field.wire != (sizeof(T) == 4 ? 5u : 1u)) return false; + if (field.size % sizeof(T)) return false; + for (size_t i = 0; i < field.size; i += sizeof(T)) { + T value; + std::memcpy(&value, field.data + i, sizeof(T)); + append(value); + } + return true; +} + +bool decode_key(const Field &message, ClipStrokeKeyframe &key) { + if (message.wire != 2) return false; + Decoder decoder{message.data, message.size}; + Field field; + while (decoder.next(field)) { + if (field.number <= 3) { + float *value = field.number == 1 ? &key.value : field.number == 2 ? &key.start : &key.end; + if (!read_reals(field, [&](float v) { *value = v; })) return false; + } else if (field.number <= 5) { + if (field.wire != 0) return false; + if (field.integer) key.tag |= field.number == 4 ? 1 : 2; + } + } + return decoder.valid; +} + +bool decode_metadata(const Field &message, Metadata &metadata) { + if (message.wire != 2) return false; + Decoder outer{message.data, message.size}; + Field variant; + while (outer.next(variant)) { + if (variant.number > 4 || metadata.kind) continue; + if (variant.wire != 2) return false; + metadata.kind = variant.number; + Decoder decoder{variant.data, variant.size}; + Field field; + while (decoder.next(field)) { + if (metadata.kind == 1) { + CGFloat *values[] = {&metadata.affine.a, &metadata.affine.b, &metadata.affine.c, + &metadata.affine.d, &metadata.affine.tx, &metadata.affine.ty}; + if (field.number <= 6 && !read_reals(field, [&](float v) { *values[field.number - 1] = v; })) return false; + } else if (metadata.kind == 2) { + if (field.number <= 2) { + if (field.wire != 0) return false; + if (field.number == 1 && field.integer <= 6) metadata.stroke.cap = int(field.integer); + if (field.number == 2 && field.integer <= 2) metadata.stroke.join = int(field.integer); + } else if (field.number == 3 && !read_reals(field, [&](float v) { metadata.dash.push_back(v); })) return false; + } else if (metadata.kind == 3) { + if (field.number <= 2) { + if (field.wire != 0) return false; + if (field.number == 1 && field.integer <= 6) metadata.boolean[0] = uint8_t(field.integer); + if (field.number == 2) metadata.boolean[1] = field.integer != 0; + } + } else if (field.number == 5) { + ClipStrokeKeyframe key{}; + if (!decode_key(field, key)) return false; + metadata.keys.push_back(key); + } else if (field.number <= 6) { + if (field.wire != 0) return false; + if (field.number == 1) { + if (field.integer <= 6) metadata.clip.cap = int(field.integer); + } else if (field.integer) { + constexpr uint32_t flags[] = {0, 0, 4, 1, 2, 0, 8}; + metadata.clip.flags |= flags[field.number]; + } + } + } + if (!decoder.valid) return false; + } + return outer.valid; +} + +unsigned metadata_kind(unsigned element) { + switch (element) { + case 4: case 16: case 18: case 22: case 23: return 0; + case 19: return 2; + case 20: return 3; + case 24: return 4; + default: return 1; + } +} + +Encoder encode_metadata(unsigned element, const void *info) { + Encoder result, value; + if (!info || element == 4 || element == 16 || element == 18 || element == 22 || element == 23) return result; + if (element == 19) { + const auto &stroke = *static_cast(info); + if (stroke.cap == 0 && stroke.join == 0 && stroke.count == 0) return result; + value.integer(1, uint64_t(int64_t(stroke.cap))); + value.integer(2, uint64_t(int64_t(stroke.join))); + if (stroke.count && stroke.values) { + Encoder dash; + auto *values = static_cast(stroke.values); + for (size_t i = 0; i < stroke.count; ++i) dash.scalar(float(values[i])); + value.data(3, dash.bytes); + } + result.data(2, value.bytes); + } else if (element == 20) { + const auto *flags = static_cast(info); + if (flags[0] == 0 && flags[1] != 1) return result; + value.integer(1, flags[0]); + value.integer(2, flags[1] == 1); + result.data(3, value.bytes); + } else if (element == 24) { + const auto &clip = *static_cast(info); + if (clip.cap == 1 && clip.count == 0) return result; + if (clip.cap != 1) { + value.varint(8); + value.varint(uint64_t(int64_t(clip.cap))); + } + value.integer(2, (clip.flags & 4) != 0); + value.integer(3, (clip.flags & 1) != 0); + value.integer(4, (clip.flags & 2) != 0); + if (clip.keys) for (size_t i = 0; i < clip.count; ++i) { + const auto &key = clip.keys[i]; + Encoder item; + item.real(1, key.value); + item.real(2, key.start); + item.real(3, key.end); + item.integer(4, (key.tag & 1) != 0); + item.integer(5, (key.tag & 2) != 0); + value.data(5, item.bytes); + } + value.integer(6, (clip.flags & 8) != 0); + result.data(4, value.bytes); + } else { + const auto &affine = *static_cast(info); + if (CGAffineTransformIsIdentity(affine)) return result; + value.real(1, float(affine.a), 1); + value.real(2, float(affine.b)); + value.real(3, float(affine.c)); + value.real(4, float(affine.d), 1); + value.real(5, float(affine.tx)); + value.real(6, float(affine.ty)); + result.data(1, value.bytes); + } + return result; +} + +} // namespace + +CFDataRef ORBPathCopyData(ORBPath path) { + struct Encoding { + Encoder output, elements, floats, doubles; + } encoding; + ORBPathApplyElements(path, &encoding, +[](void *context, ORBPathElement element, const CGFloat *points, const void *info) { + auto &encoding = *static_cast(context); + unsigned type = unsigned(element); + Encoder metadata = encode_metadata(type, info); + uint8_t code = uint8_t(type); + if (!metadata.bytes.empty()) { + code |= 0x80; + encoding.output.data(4, metadata.bytes); + } + size_t count = Storage::argument_count(element); + for (size_t i = 0; i < count; ++i) if (!(std::abs(points[i]) <= 32767)) code |= 0x40; + encoding.elements.bytes.push_back(code); + for (size_t i = 0; i < count; ++i) { + if (code & 0x40) encoding.doubles.scalar(points[i]); + else encoding.floats.scalar(float(points[i])); + } + return true; + }); + if (!encoding.elements.bytes.empty()) encoding.output.data(3, encoding.elements.bytes); + if (!encoding.floats.bytes.empty()) encoding.output.data(2, encoding.floats.bytes); + if (!encoding.doubles.bytes.empty()) encoding.output.data(5, encoding.doubles.bytes); + return CFDataCreate(nullptr, encoding.output.bytes.data(), CFIndex(encoding.output.bytes.size())); +} + +ORBPath ORBPathMakeWithData(CFDataRef data) { + if (!data) return ORBPathNull; + Decoder decoder{CFDataGetBytePtr(data), size_t(CFDataGetLength(data))}; + Bytes elements; + std::vector floats, doubles; + std::vector metadata; + Field field; + while (decoder.next(field)) { + switch (field.number) { + case 1: case 3: + if (field.wire != 2) { decoder.valid = false; break; } + elements.assign(field.data, field.data + field.size); + if (field.number == 1) for (auto &element : elements) element &= 0x1f; + break; + case 2: + if (!read_reals(field, [&](float v) { floats.push_back(v); })) return ORBPathNull; + break; + case 4: + metadata.emplace_back(); + if (!decode_metadata(field, metadata.back())) return ORBPathNull; + break; + case 5: + if (!read_reals(field, [&](double v) { doubles.push_back(v); })) return ORBPathNull; + break; + default: break; + } + } + // The format accepts a valid decoded prefix after a truncated field. + size_t floatIndex = 0, doubleIndex = 0, metadataIndex = 0, depth = 0; + // Validate the complete stream before Storage reads any payload. + for (uint8_t code : elements) { + unsigned type = code & 0x3f; + if (type > 24) return ORBPathNull; + if (code & 0x80) { + if (metadataIndex == metadata.size() || metadata[metadataIndex++].kind != metadata_kind(type)) return ORBPathNull; + } + size_t count = Storage::argument_count(ORBPathElement(type)); + auto &index = code & 0x40 ? doubleIndex : floatIndex; + const auto &values = code & 0x40 ? doubles : floats; + if (count > values.size() - index) return ORBPathNull; + index += count; + if ((uint32_t(1) << type) & 0x1de0000) ++depth; + else if (type == 16) { + if (!depth) return ORBPathNull; + --depth; + } + } + if (depth) return ORBPathNull; + + alignas(Storage) uint8_t buffer[144]; + auto &storage = *new (buffer) Storage(sizeof(buffer)); + floatIndex = doubleIndex = metadataIndex = 0; + for (uint8_t code : elements) { + auto type = ORBPathElement(code & 0x3f); + const void *info = code & 0x80 ? metadata[metadataIndex++].pointer() : nullptr; + auto &index = code & 0x40 ? doubleIndex : floatIndex; + const auto &values = code & 0x40 ? doubles : floats; + size_t count = Storage::argument_count(type); + storage.append_element(type, count ? values.data() + index : nullptr, info); + index += count; + } + ORBPath result = ORB::Path::copy_storage_path(storage); + storage.~Storage(); + return result; +} + +CFStringRef ORBPathCopyDebugDescription(ORBPath path) { + auto result = CFStringCreateMutable(nullptr, 0); + CFStringAppend(result, CFSTR("RBPath:")); + ORBPathApplyElements(path, result, +[](void *context, ORBPathElement element, const CGFloat *points, const void *info) { + auto result = static_cast(context); + constexpr const char *names[] = { + "moveto", "lineto", "quadto", "cubeto", "close", "rect", "rect*", "ellipse", + "rounded-rect", "continuous-rect", "uneven-rounded-rect", "uneven-continuous-rect", + "relative-arc", "clockwise-arc", "counter-clockwise-arc", "arc-to-point", "pop", + "push affine", "push cut", "push stroke", "push bool", "end-path", "push-dilate-glyph", + "push offset", "push clip-stroke" + }; + constexpr const char *caps[] = {"butt", "round", "square", "outwards-triangle", "inwards-triangle", "forwards-triangle", "backwards-triangle"}; + constexpr const char *joins[] = {"miter", "round", "bevel"}; + constexpr const char *operations[] = {"normalize", "intersect", "union", "subtract", "symmetric-difference", "line-intersect", "line-subtract"}; + unsigned type = unsigned(element); + CFStringAppendFormat(result, nullptr, CFSTR("\n %s"), type < 25 ? names[type] : "unknown"); + for (size_t i = 0; i < Storage::argument_count(element); ++i) CFStringAppendFormat(result, nullptr, CFSTR(" %g"), points[i]); + if (!info) return true; + switch (metadata_kind(type)) { + case 1: { + const auto &t = *static_cast(info); + CFStringAppendFormat(result, nullptr, CFSTR(" affine(%g %g; %g %g; %g %g)"), t.a, t.b, t.c, t.d, t.tx, t.ty); + break; + } + case 2: { + const auto &stroke = *static_cast(info); + CFStringAppendFormat(result, nullptr, CFSTR(" %s %s ["), unsigned(stroke.cap) < 7 ? caps[stroke.cap] : "unknown", unsigned(stroke.join) < 3 ? joins[stroke.join] : "unknown"); + const auto *values = static_cast(stroke.values); + if (values) for (size_t i = 0; i < stroke.count; ++i) CFStringAppendFormat(result, nullptr, CFSTR("%s%g"), i ? ", " : "", values[i]); + CFStringAppend(result, CFSTR("]")); + break; + } + case 3: { + const auto *flags = static_cast(info); + CFStringAppendFormat(result, nullptr, CFSTR(" %s, %s"), flags[0] < 7 ? operations[flags[0]] : "unknown", flags[1] == 1 ? "even-odd" : "non-zero"); + break; + } + case 4: { + const auto &clip = *static_cast(info); + CFStringAppendFormat(result, nullptr, CFSTR(" %s ["), unsigned(clip.cap) < 7 ? caps[clip.cap] : "unknown"); + if (clip.keys) for (size_t i = 0; i < clip.count; ++i) { + const auto &key = clip.keys[i]; + CFStringAppendFormat(result, nullptr, CFSTR("%s%s%s%g:%g,%g"), i ? ", " : "", key.tag & 1 ? "^" : "", key.tag & 2 ? "*" : "", double(key.value), double(key.start), double(key.end)); + } + CFStringAppend(result, CFSTR("]")); + break; + } + } + return true; + }); + CFStringAppend(result, CFSTR("\n")); + return result; +} + +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/PathEnumeration.cpp b/Sources/OpenRenderBoxCxx/Path/PathEnumeration.cpp new file mode 100644 index 0000000..30752e5 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/PathEnumeration.cpp @@ -0,0 +1,241 @@ +// +// PathEnumeration.cpp +// OpenRenderBox + +#include + +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN + +#include +#include "PathMapper.hpp" +#include +#include +#if defined(__aarch64__) +#include +#endif + +namespace { + +template struct Point { + T x, y; +}; + +template struct CubicIterator { + Point point, first, second, third; + + CubicIterator(const Point (&p)[4], int count) : point(p[0]) { + float step = 1.0f / count; + float square = step * step, cube = square * step; + auto initialize = [=](T p0, T p1, T p2, T p3, T &d1, T &d2, T &d3) { + T a = (p1 - p0) * T(3); + T b = std::fma(T(3), p2 - p1, -a); + T c = p3 - (std::fma(T(3), p1 - p0, p0) + b); + d3 = (c * T(6)) * T(cube); + b *= T(square); + d2 = std::fma(T(2), b, d3); + d1 = std::fma(c, T(cube), std::fma(a, T(step), b)); + }; + initialize(p[0].x, p[1].x, p[2].x, p[3].x, first.x, second.x, third.x); + initialize(p[0].y, p[1].y, p[2].y, p[3].y, first.y, second.y, third.y); + } + + void advance() { + point.x += first.x; + point.y += first.y; + first.x += second.x; + first.y += second.y; + second.x += third.x; + second.y += third.y; + } +}; + +CGPoint quadratic_control(CGPoint end, CGPoint control) { + return {std::fma(control.x, 2.0, end.x) * (1.0 / 3.0), + std::fma(control.y, 2.0, end.y) * (1.0 / 3.0)}; +} + +double distance(CGPoint first, CGPoint second) { + double x = second.x - first.x, y = second.y - first.y; + return std::sqrt(x * x + y * y); +} + +float reciprocal(float value) { +#if defined(__aarch64__) + float32x2_t input = vset_lane_f32(value, vdup_n_f32(0), 0); + float32x2_t result = vrecpe_f32(input); + result = vmul_f32(vrecps_f32(input, result), result); + result = vmul_f32(result, vrecps_f32(input, result)); + return vget_lane_f32(result, 0); +#else + return 1.0f / value; +#endif +} + +struct ApplyContext { + const CGAffineTransform *transform; + void *info; + ORBPathApplierCallback callback; + bool active = true; + + void apply(const CGPathElement &element) { + if (!active) { return; } + CGPoint points[3]; + static constexpr size_t counts[] = {1, 1, 2, 3, 0}; + size_t count = counts[element.type]; + for (size_t i = 0; i < count; ++i) { + CGPoint point = element.points[i]; + if (transform) { + points[i] = { + std::fma(transform->c, point.y, std::fma(transform->a, point.x, transform->tx)), + std::fma(transform->d, point.y, std::fma(transform->b, point.x, transform->ty)) + }; + } else { + points[i] = point; + } + } + CGPathElement mapped{element.type, count ? points : nullptr}; + active = callback(info, &mapped); + } +}; + +struct Flattener { + void *info; + ORBPathLineApplyCallback callback; + float factor; + bool active = true; + CGPoint start = CGPointZero, current = CGPointZero; + + void emit(ORBPathLineElement element, CGPoint point) { + if (active) { active = callback(info, element, &point); } + current = point; + } + + void cubic(CGPoint first, CGPoint second, CGPoint end) { + if (!active) { current = end; return; } + Point p[] = {{float(current.x), float(current.y)}, {float(first.x), float(first.y)}, + {float(second.x), float(second.y)}, {float(end.x), float(end.y)}}; + auto curvature = [](float p0, float p1, float p2, float p3) { + return std::fmax(std::abs((p2 - (p1 + p1)) + p0), std::abs((p1 - (p2 + p2)) + p3)); + }; + float x = curvature(p[0].x, p[1].x, p[2].x, p[3].x); + float y = curvature(p[0].y, p[1].y, p[2].y, p[3].y); + float estimate = std::ceil(std::sqrt(std::sqrt(x * x + y * y)) * factor); + if (!std::isfinite(estimate)) { current = end; return; } + if (estimate < 1 && p[0].x == p[3].x && p[0].y == p[3].y) { return; } + int count = int(std::fmin(std::fmax(estimate, 1), 1024)); + if (count >= 2) { + CubicIterator iterator(p, count); + for (int i = 1; i < count && active; ++i) { + iterator.advance(); + emit(ORBPathLineElementAddLineToPoint, {iterator.point.x, iterator.point.y}); + } + } + if (active) { emit(ORBPathLineElementAddLineToPoint, end); } + } + + void apply(const CGPathElement &element) { + switch (element.type) { + case kCGPathElementMoveToPoint: + start = element.points[0]; + emit(ORBPathLineElementMoveToPoint, start); + break; + case kCGPathElementAddLineToPoint: + emit(ORBPathLineElementAddLineToPoint, element.points[0]); + break; + case kCGPathElementAddQuadCurveToPoint: + cubic(quadratic_control(current, element.points[0]), + quadratic_control(element.points[1], element.points[0]), element.points[1]); + break; + case kCGPathElementAddCurveToPoint: + cubic(element.points[0], element.points[1], element.points[2]); + break; + case kCGPathElementCloseSubpath: + emit(ORBPathLineElementCloseSubpath, CGPointZero); + current = start; + break; + } + } +}; + +struct LengthAccumulator { + double tolerance; + double length = 0; + CGPoint start = CGPointZero, current = CGPointZero; + + void cubic(CGPoint first, CGPoint second, CGPoint end) { + Point p[] = {{current.x, current.y}, {first.x, first.y}, + {second.x, second.y}, {end.x, end.y}}; + auto curvature = [](double p0, double p1, double p2, double p3) { + return std::fmax(std::abs((p0 - (p1 + p1)) + p2), std::abs((p1 - (p2 + p2)) + p3)); + }; + double x = curvature(p[0].x, p[1].x, p[2].x, p[3].x); + double y = curvature(p[0].y, p[1].y, p[2].y, p[3].y); + double estimate = std::ceil(std::sqrt((std::sqrt(x * x + y * y) * 0.75) / tolerance)); + if (std::isfinite(estimate)) { + int count = estimate < 1 ? (CGPointEqualToPoint(current, end) ? 0 : 1) : int(std::fmin(estimate, 1024)); + if (count) { + CubicIterator iterator(p, count); + for (int i = 0; i < count; ++i) { + length += std::sqrt(iterator.first.x * iterator.first.x + iterator.first.y * iterator.first.y); + iterator.advance(); + } + } + } + current = end; + } + + void apply(const CGPathElement &element) { + switch (element.type) { + case kCGPathElementMoveToPoint: + current = start = element.points[0]; + break; + case kCGPathElementAddLineToPoint: + length += distance(current, element.points[0]); + current = element.points[0]; + break; + case kCGPathElementAddQuadCurveToPoint: + cubic(quadratic_control(current, element.points[0]), + quadratic_control(element.points[1], element.points[0]), element.points[1]); + break; + case kCGPathElementAddCurveToPoint: + cubic(element.points[0], element.points[1], element.points[2]); + break; + case kCGPathElementCloseSubpath: + length += distance(current, start); + current = start; + break; + } + } +}; + +} // namespace + +bool ORBPathApply(ORBPath path, const CGAffineTransform *transform, void *info, ORBPathApplierCallback callback) { + ApplyContext context{transform, info, callback}; + ORB::Path::apply_mapped_path(path, &context, +[](void *info, const CGPathElement *element) { + auto &context = *static_cast(info); + context.apply(*element); + return context.active; + }); + return context.active; +} + +bool ORBPathApplyLines(ORBPath path, double tolerance, void *info, ORBPathLineApplyCallback callback) { + Flattener flattener{info, callback, 0.75f * reciprocal(float(tolerance))}; + ORBPathApply(path, nullptr, &flattener, +[](void *info, const CGPathElement *element) { + static_cast(info)->apply(*element); + return true; + }); + return flattener.active; +} + +double ORBPathGetLength(ORBPath path, double tolerance, const CGAffineTransform *transform) { + LengthAccumulator accumulator{tolerance}; + ORBPathApply(path, transform, &accumulator, +[](void *info, const CGPathElement *element) { + static_cast(info)->apply(*element); + return true; + }); + return accumulator.length; +} + +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/PathGeometry.hpp b/Sources/OpenRenderBoxCxx/Path/PathGeometry.hpp new file mode 100644 index 0000000..33b68a4 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/PathGeometry.hpp @@ -0,0 +1,127 @@ +// +// PathGeometry.hpp +// OpenRenderBox + +#pragma once + +#include + +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN + +#include +#include + +namespace ORB::Path::Geometry { + +inline CGPoint mix(CGPoint a, CGPoint b, CGFloat t) { + return {std::fma(b.x - a.x, t, a.x), std::fma(b.y - a.y, t, a.y)}; +} + +inline CGFloat distance(CGPoint a, CGPoint b) { + CGFloat x = b.x - a.x, y = b.y - a.y; + return std::sqrt(x * x + y * y); +} + +struct Cubic { + CGPoint p[4]; + + void split(CGFloat t, Cubic &left, Cubic &right) const { + CGPoint a = mix(p[0], p[1], t), b = mix(p[1], p[2], t), c = mix(p[2], p[3], t); + CGPoint d = mix(a, b, t), e = mix(b, c, t), f = mix(d, e, t); + left = {{p[0], a, d, f}}; + right = {{f, e, c, p[3]}}; + } +}; + +struct Quadratic { + CGPoint p[3]; + + void split(CGFloat t, Quadratic &left, Quadratic &right) const { + CGPoint a = mix(p[0], p[1], t), b = mix(p[1], p[2], t); + CGPoint middle = mix(a, b, t); + left = {{p[0], a, middle}}; + right = {{middle, b, p[2]}}; + } +}; + +inline CGFloat quadratic_length(Quadratic curve, unsigned depth = 0) { + CGFloat x = (curve.p[0].x - curve.p[1].x) + (curve.p[2].x - curve.p[1].x); + CGFloat y = (curve.p[0].y - curve.p[1].y) + (curve.p[2].y - curve.p[1].y); + CGFloat curvature = x * x + y * y; + if (depth > 32 || curvature < 4 || !std::isfinite(curvature)) { + return distance(curve.p[0], curve.p[2]); + } + Quadratic left, right; + curve.split(0.5, left, right); + return quadratic_length(left, depth + 1) + quadratic_length(right, depth + 1); +} + +inline CGFloat quadratic_measure(Quadratic curve, CGFloat requested, bool findIndex) { + if (!findIndex) { return quadratic_length(curve); } + if (requested <= 0) { return 0; } + if (quadratic_length(curve) <= requested) { return 1; } + CGFloat lower = 0, upper = 1; + do { + CGFloat middle = (lower + upper) * 0.5; + Quadratic left, right; + curve.split(middle, left, right); + if (quadratic_length(left) > requested) { + upper = middle; + } else { + lower = middle; + } + } while (upper - lower > 0.0001); + return (lower + upper) * 0.5; +} + +struct CurveSpan { + Cubic curve; + CGFloat interval; +}; + +inline CGFloat curve_measure(Cubic curve, CGFloat requested, bool findIndex) { + CGFloat edge0 = distance(curve.p[0], curve.p[1]); + CGFloat edge2 = distance(curve.p[2], curve.p[3]); + if (!std::isfinite(edge0 + edge2) || (findIndex && requested == 0)) { + return 0; + } + CurveSpan pending[29]; + size_t count = 0; + CGFloat interval = 1, parameter = 0, total = 0; + for (unsigned iteration = 0; iteration < 1024; ++iteration) { + edge0 = distance(curve.p[0], curve.p[1]); + CGFloat edge1 = distance(curve.p[1], curve.p[2]); + edge2 = distance(curve.p[2], curve.p[3]); + CGFloat polygon = edge0 + edge2 + edge1; + CGFloat chord = distance(curve.p[0], curve.p[3]); + bool flat = polygon - chord <= 0.5 * interval; + if (findIndex) { + flat = flat && std::abs(edge0 - edge1) + std::abs(edge1 - edge2) < 0.5; + } + if (flat || count == 29) { + CGFloat length = (polygon + chord) * 0.5; + if (findIndex && total + length >= requested) { + return std::fmin(parameter + (requested - total) * interval / length, 1); + } + total += length; + if (count == 0) { + return findIndex ? 1 : total; + } + parameter += interval; + auto span = pending[--count]; + curve = span.curve; + interval = span.interval; + } else { + Cubic left, right; + curve.split(0.5, left, right); + interval *= 0.5; + pending[count++] = {right, interval}; + curve = left; + } + } + return 1; +} + +} // namespace ORB::Path::Geometry + +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/PathInterpolation.cpp b/Sources/OpenRenderBoxCxx/Path/PathInterpolation.cpp new file mode 100644 index 0000000..33a050f --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/PathInterpolation.cpp @@ -0,0 +1,204 @@ +// +// PathInterpolation.cpp +// OpenRenderBox + +#include "PathInterpolation.hpp" + +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN + +#include "NestedCallbacks.hpp" +#include "PathStorageObject.hpp" +#include "Storage.hpp" +#include +#include +#include +#include + +namespace ORB::Path { +namespace { + +bool has_affine_info(ORBPathElement element) { + auto type = static_cast(element); + return type <= 24 && ((1u << type) & 0x1dd0010u) == 0; +} + +bool identity(const CGAffineTransform &t) { + return t.a == 1 && t.b == 0 && t.c == 0 && t.d == 1 && t.tx == 0 && t.ty == 0; +} + +struct Record { + ORBPathElement type; + CGFloat values[8] = {}; + CGAffineTransform transform{1, 0, 0, 1, 0, 0}; + bool hasTransform = false; + uint8_t boolean[2] = {}; + int32_t flags = 0; + int32_t cap = 1; +}; + +std::vector collect(ORBPath path) { + std::vector records; + ORBPathApplyElements(path, &records, +[](void *context, ORBPathElement type, const CGFloat *values, const void *info) { + Record record{type}; + std::copy_n(values, Storage::argument_count(type), record.values); + if (info) { + if (type == ORBPathElementBoolean) std::memcpy(record.boolean, info, sizeof(record.boolean)); + else if (type == ORBPathElementClipStroke) { + const auto &properties = *static_cast(info); + record.flags = properties.cap; + record.cap = properties.join; + } else if (has_affine_info(type)) { + record.transform = *static_cast(info); + record.hasTransform = !identity(record.transform); + } + } + static_cast *>(context)->push_back(record); + return true; + }); + return records; +} + +bool compatible_types(unsigned from, unsigned to, bool trimmed) { + if (from == to) return true; + bool fromRect = from == 6 || (from == 5 && !trimmed); + bool toRect = to == 6 || (to == 5 && !trimmed); + bool fromRounded = from >= 8 && from <= 11; + bool toRounded = to >= 8 && to <= 11; + return (fromRect && (toRect || toRounded)) || (fromRounded && toRect); +} + +double interpolate(double from, double to, double amount) { + double result = std::fma(to - from, amount, from); + return std::isnan(result) ? from : result; +} + +CGAffineTransform interpolate_transform(CGAffineTransform from, CGAffineTransform to, float fraction) { + double amount = fraction; + auto mix = [amount](double a, double b) { return std::fma(b - a, amount, a); }; + if (from.a == to.a && from.b == to.b && from.c == to.c && from.d == to.d) + return {from.a, from.b, from.c, from.d, mix(from.tx, to.tx), mix(from.ty, to.ty)}; + if ((from.b == 0 && from.c == 0 && to.b == 0 && to.c == 0) || + (from.a == 0 && from.d == 0 && to.a == 0 && to.d == 0)) + return {mix(from.a, to.a), mix(from.b, to.b), mix(from.c, to.c), mix(from.d, to.d), + mix(from.tx, to.tx), mix(from.ty, to.ty)}; + + auto a = CGAffineTransformDecompose(from), b = CGAffineTransformDecompose(to); + if ((a.scale.width < 0 && b.scale.height < 0) || (a.scale.height < 0 && b.scale.width < 0)) { + a.scale.width = -a.scale.width; + a.scale.height = -a.scale.height; + a.rotation += a.rotation < 0 ? M_PI : -M_PI; + } + if (a.rotation == 0) a.rotation = 2 * M_PI; + if (b.rotation == 0) b.rotation = 2 * M_PI; + if (std::abs(a.rotation - b.rotation) > M_PI) { + if (a.rotation > b.rotation) a.rotation -= 2 * M_PI; + else b.rotation -= 2 * M_PI; + } + CGAffineTransformComponents result{ + {mix(a.scale.width, b.scale.width), mix(a.scale.height, b.scale.height)}, + mix(a.horizontalShear, b.horizontalShear), mix(a.rotation, b.rotation), + {mix(a.translation.dx, b.translation.dx), mix(a.translation.dy, b.translation.dy)}, + }; + return CGAffineTransformMakeWithComponents(result); +} + +} // namespace + +bool can_interpolate(ORBPath from, ORBPath to) { + if (ORBPathEqualToPath(from, to)) return true; + auto records = collect(from); + struct Comparison { + const std::vector &records; + size_t position = 0; + std::vector trimmed{false}; + } comparison{records}; + bool result = ORBPathApplyElements(to, &comparison, +[](void *context, ORBPathElement type, const CGFloat *values, const void *info) { + auto &state = *static_cast(context); + if (NestedCallbacks::is_nesting_element(type)) + state.trimmed.push_back(state.trimmed.back() || type == ORBPathElementTrimmed); + else if (type == ORBPathElementEnd && state.trimmed.size() > 1) state.trimmed.pop_back(); + if (state.position == state.records.size()) return false; + const auto &record = state.records[state.position++]; + if (!compatible_types(record.type, type, state.trimmed.back())) return false; + if (type == ORBPathElementBoolean) { + uint8_t properties[2] = {}; + if (info) std::memcpy(properties, info, sizeof(properties)); + return std::equal(std::begin(record.boolean), std::end(record.boolean), properties); + } + if (type == ORBPathElementClipStroke) { + Storage::StrokeInfo defaults{0, 1, 0, nullptr}; + const auto &properties = info ? *static_cast(info) : defaults; + int32_t a = record.flags, b = properties.cap; + if (((a ^ b) & 1) && (record.values[0] < 0.0001 || record.values[0] > 0.9999) && + std::abs(record.values[0] + (values[0] - 1)) < 0.0001) { + a &= ~1; + b &= ~1; + } + return a == b && record.cap == properties.join; + } + return true; + }); + return result && comparison.position == records.size(); +} + +ORBPath copy_interpolated_path(ORBPath from, ORBPath to, float amount) { + if (amount == 0 || (from.storage == to.storage && from.callbacks == to.callbacks)) return ORBPathRetain(from); + if (amount == 1) return ORBPathRetain(to); + // The result takes discrete metadata from the second path, including + // metadata for which the compatibility query returns false. + auto records = collect(from); + Storage storage(sizeof(Storage)); + struct Mixing { + const std::vector &records; + Storage &storage; + float amount; + size_t position = 0; + } mixing{records, storage, amount}; + bool result = ORBPathApplyElements(to, &mixing, +[](void *context, ORBPathElement type, const CGFloat *values, const void *info) { + auto &state = *static_cast(context); + // The callback ABI permits arbitrary streams. Do not read beyond the + // first stream when a caller skips the compatibility query. + if (state.position == state.records.size()) return false; + const auto &record = state.records[state.position++]; + unsigned outputType = record.type; + size_t count = Storage::argument_count(record.type); + CGFloat left[8], right[8] = {}, output[8] = {}; + std::copy_n(record.values, count, left); + std::copy_n(values, Storage::argument_count(type), right); + if (record.type != type) { + bool fromRect = record.type == 5 || record.type == 6; + bool toRect = type == 5 || type == 6; + bool fromRounded = record.type >= 8 && record.type <= 11; + bool toRounded = type >= 8 && type <= 11; + if (fromRect && toRounded) { + outputType = type; + count = Storage::argument_count(type); + std::fill(left + 4, left + count, 0); + } else if (!(fromRounded && toRect) && !(fromRect && toRect)) return false; + } + for (size_t i = 0; i < count; ++i) output[i] = interpolate(left[i], right[i], state.amount); + + CGAffineTransform transform; + if (has_affine_info(type)) { + CGAffineTransform target = info ? *static_cast(info) : CGAffineTransformIdentity; + if (record.hasTransform || !identity(target)) { + transform = interpolate_transform(record.transform, target, state.amount); + info = &transform; + } + } + return state.storage.append_element(static_cast(outputType), output, info); + }); + return result ? copy_storage_path(storage) : ORBPathNull; +} + +} // namespace ORB::Path + +extern "C" bool ORBPathCanInterpolate(ORBPath from, ORBPath to) { + return ORB::Path::can_interpolate(from, to); +} + +extern "C" ORBPath ORBPathCopyInterpolatedPath(ORBPath from, ORBPath to, float amount) { + return ORB::Path::copy_interpolated_path(from, to, amount); +} + +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/PathInterpolation.hpp b/Sources/OpenRenderBoxCxx/Path/PathInterpolation.hpp new file mode 100644 index 0000000..8006fd9 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/PathInterpolation.hpp @@ -0,0 +1,18 @@ +// +// PathInterpolation.hpp +// OpenRenderBox + +#pragma once + +#include + +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN + +#include + +namespace ORB::Path { +bool can_interpolate(ORBPath from, ORBPath to); +ORBPath copy_interpolated_path(ORBPath from, ORBPath to, float amount); +} + +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/PathMapper.cpp b/Sources/OpenRenderBoxCxx/Path/PathMapper.cpp new file mode 100644 index 0000000..4786f3a --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/PathMapper.cpp @@ -0,0 +1,1075 @@ +// +// PathMapper.cpp +// OpenRenderBox + +#include + +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN + +#include "PathMapper.hpp" +#include "PathClipStroke.hpp" +#include "PathGeometry.hpp" +#include "RoundedRect.hpp" +#include "Storage.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace ORB { +namespace Path { +namespace { + +using namespace Geometry; + +void add_relative_arc(CGMutablePathRef path, const CGAffineTransform *transform, + CGPoint center, CGFloat radius, CGFloat start, CGFloat delta) { + CGFloat sine = std::sin(start), cosine = std::cos(start); + CGPoint point{std::fma(radius, cosine, center.x), std::fma(radius, sine, center.y)}; + if (CGPathIsEmpty(path)) { + CGPathMoveToPoint(path, transform, point.x, point.y); + } else { + CGPathAddLineToPoint(path, transform, point.x, point.y); + } + if (!std::isfinite(delta)) { return; } + + CGFloat sign = delta < 0 ? -1 : 1; + CGFloat quarter = sign * M_PI_2; + CGFloat tangent = sign * 0.5522847498307932 * radius; + unsigned remaining = 1023; + auto curve = [&](CGFloat nextSine, CGFloat nextCosine, CGFloat factor) { + CGPoint end{std::fma(radius, nextCosine, center.x), std::fma(radius, nextSine, center.y)}; + CGPathAddCurveToPoint(path, transform, + std::fma(-sine, factor, point.x), std::fma(cosine, factor, point.y), + std::fma(nextSine, factor, end.x), std::fma(-nextCosine, factor, end.y), end.x, end.y); + point = end; + sine = nextSine; + cosine = nextCosine; + }; + while (delta > M_PI_2 || delta < -M_PI_2) { + if (remaining-- == 0) { return; } + curve(cosine * sign, -sine * sign, tangent); + start += quarter; + delta -= quarter; + } + // Compact storage rounds angles to Float. Ignore the residual after exact quarter turns. + if (std::abs(delta) <= static_cast(1e-5f)) { return; } + CGFloat half = delta * 0.5; + CGFloat factor = std::fma(-std::cos(half), 4, 4) * radius / (3 * std::sin(half)); + curve(std::sin(start + delta), std::cos(start + delta), factor); +} + +void add_arc(CGMutablePathRef path, const CGAffineTransform *transform, CGPoint center, + CGFloat radius, CGFloat start, CGFloat end, bool clockwise) { + constexpr CGFloat turn = 2 * M_PI; + if (CGPathIsEmpty(path) && start == 0 && + (clockwise ? end >= turn && end - turn < 1e-5 + : std::abs(end - turn) < static_cast(1e-5f))) { + constexpr CGFloat k = 0.5522847498; + CGFloat r = std::abs(radius), y = clockwise ? -r : r; + CGAffineTransform ellipse = CGAffineTransformMake(r, 0, 0, y, center.x, center.y); + if (transform) { ellipse = CGAffineTransformConcat(ellipse, *transform); } + CGPathMoveToPoint(path, &ellipse, 1, 0); + CGPathAddCurveToPoint(path, &ellipse, 1, k, k, 1, 0, 1); + CGPathAddCurveToPoint(path, &ellipse, -k, 1, -1, k, -1, 0); + CGPathAddCurveToPoint(path, &ellipse, -1, -k, -k, -1, 0, -1); + CGPathAddCurveToPoint(path, &ellipse, k, -1, 1, -k, 1, 0); + CGPathCloseSubpath(path); + return; + } + if (clockwise && start < end) { + start = std::fma(std::ceil((end - start) / turn), turn, start); + } else if (!clockwise && end < start) { + end = std::fma(std::ceil((start - end) / turn), turn, end); + } + add_relative_arc(path, transform, center, radius, start, end - start); +} + +struct Segment { + CGPathElementType type; + Cubic curve; + CGFloat length; +}; + +struct TrimInput { + std::vector segments; + CGPoint current = CGPointZero; + CGPoint start = CGPointZero; + CGFloat length = 0; + + void append(const CGPathElement &element) { + Cubic curve{{current, {}, {}, {}}}; + CGFloat segmentLength = 0; + switch (element.type) { + case kCGPathElementMoveToPoint: + current = start = element.points[0]; + curve.p[0] = current; + break; + case kCGPathElementAddLineToPoint: + curve.p[3] = element.points[0]; + segmentLength = distance(current, curve.p[3]); + current = curve.p[3]; + break; + case kCGPathElementAddQuadCurveToPoint: + curve.p[1] = {(current.x + 2 * element.points[0].x) / 3, (current.y + 2 * element.points[0].y) / 3}; + curve.p[2] = {(element.points[1].x + 2 * element.points[0].x) / 3, (element.points[1].y + 2 * element.points[0].y) / 3}; + curve.p[3] = element.points[1]; + segmentLength = curve_measure(curve, 0, false); + current = curve.p[3]; + break; + case kCGPathElementAddCurveToPoint: + std::copy_n(element.points, 3, curve.p + 1); + segmentLength = curve_measure(curve, 0, false); + current = curve.p[3]; + break; + case kCGPathElementCloseSubpath: + curve.p[3] = start; + segmentLength = distance(current, start); + current = start; + break; + } + length += segmentLength; + segments.push_back({element.type, curve, segmentLength}); + } +}; + +CGPathRef copy_trimmed_path(CGPathRef path, float from, float to) { + if (from <= 0 && to >= 1) { + return CGPathCreateCopy(path); + } + CGMutablePathRef result = CGPathCreateMutable(); + if (!(from < to)) { + return result; + } + TrimInput input; + CGPathApply(path, &input, +[](void *context, const CGPathElement *element) { + static_cast(context)->append(*element); + }); + CGFloat fromDistance = input.length * from, toDistance = input.length * to; + float consumed = 0; + bool needsMove = true, includesStart = false; + for (const auto &segment : input.segments) { + if (segment.type == kCGPathElementMoveToPoint) { + needsMove = true; + includesStart = false; + continue; + } + CGFloat begin = consumed, end = begin + segment.length; + consumed = static_cast(end); + if (segment.length == 0) { + if (segment.type == kCGPathElementCloseSubpath && includesStart && begin <= toDistance) { + CGPathCloseSubpath(result); + } + continue; + } + CGFloat first = fromDistance >= begin ? std::fmax((fromDistance - begin) / segment.length, 0) : 0; + CGFloat last = toDistance < end ? std::fmin((toDistance - begin) / segment.length, 1) : 1; + if (!(first < last)) { + continue; + } + if (needsMove) { + includesStart = first == 0; + } + if (segment.type == kCGPathElementAddLineToPoint || segment.type == kCGPathElementCloseSubpath) { + CGPoint start = mix(segment.curve.p[0], segment.curve.p[3], first); + CGPoint finish = mix(segment.curve.p[0], segment.curve.p[3], last); + if (needsMove) { + CGPathMoveToPoint(result, nullptr, start.x, start.y); + needsMove = false; + } + CGPathAddLineToPoint(result, nullptr, finish.x, finish.y); + if (segment.type == kCGPathElementCloseSubpath && last == 1 && includesStart) { + CGPathCloseSubpath(result); + } + } else { + Cubic curve = segment.curve; + if (first > 0 || last < 1) { + if (first > 0.001) { + first = curve_measure(curve, first * segment.length, true); + } + if (last < 0.999) { + last = curve_measure(curve, last * segment.length, true); + } + Cubic left, right; + if (first > 0.001) { + curve.split(first, left, right); + curve = right; + last = (last - first) / (1 - first); + } + if (last < 0.999) { + curve.split(last, left, right); + curve = left; + } + } + if (needsMove) { + CGPathMoveToPoint(result, nullptr, curve.p[0].x, curve.p[0].y); + needsMove = false; + } + CGPathAddCurveToPoint(result, nullptr, curve.p[1].x, curve.p[1].y, curve.p[2].x, curve.p[2].y, curve.p[3].x, curve.p[3].y); + } + } + return result; +} + + +struct Dilator { + CGMutablePathRef path; + CGPoint amount; + CGPoint current = CGPointZero; + CGPoint start = CGPointZero; + bool needsMove = true; + + CGPoint begin(CGPoint end) { + CGFloat x = end.y < current.y ? -amount.x : end.y > current.y ? amount.x : 0; + CGFloat y = end.x < current.x ? amount.y : end.x > current.x ? -amount.y : 0; + CGPoint offset{x, amount.y + y}; + if (needsMove) { + CGPathMoveToPoint(path, nullptr, current.x + offset.x, current.y + offset.y); + needsMove = false; + } else { + CGPathAddLineToPoint(path, nullptr, current.x + offset.x, current.y + offset.y); + } + return offset; + } + + void line(CGPoint end) { + CGPoint offset = begin(end); + CGPathAddLineToPoint(path, nullptr, end.x + offset.x, end.y + offset.y); + current = end; + } + + void close() { + if (!needsMove) { + if (!CGPointEqualToPoint(current, start)) { + line(start); + } + CGPathCloseSubpath(path); + needsMove = true; + } + } + + void move(CGPoint point) { + close(); + current = start = point; + } + + void quad(CGPoint control, CGPoint end, unsigned axis = 0) { + if (axis < 2) { + CGFloat first = axis == 0 ? current.x : current.y; + CGFloat middle = axis == 0 ? control.x : control.y; + CGFloat last = axis == 0 ? end.x : end.y; + // Preserve the source consumer's split rule, including its denominator. + CGFloat t = (first - middle) / (first - last); + if (first != last && t >= 0.0001 && t <= 0.9999) { + CGPoint a = mix(current, control, t), b = mix(control, end, t); + quad(a, mix(a, b, t), axis + 1); + control = b; + } + quad(control, end, axis + 1); + return; + } + CGPoint offset = begin(end); + CGPathAddQuadCurveToPoint(path, nullptr, control.x + offset.x, control.y + offset.y, end.x + offset.x, end.y + offset.y); + current = end; + } + + void cubic(Cubic curve, unsigned axis = 0) { + if (axis < 2) { + auto coordinate = [axis](CGPoint p) { return axis == 0 ? p.x : p.y; }; + CGFloat p0 = coordinate(curve.p[0]), p1 = coordinate(curve.p[1]); + CGFloat p2 = coordinate(curve.p[2]), p3 = coordinate(curve.p[3]); + CGFloat a = std::fma(p1 - p2, 3, p3 - p0); + CGFloat b = 2 * (p2 - (p1 + p1) + p0), c = p1 - p0; + CGFloat discriminant = std::fma(b, b, a * (-4 * c)); + if (discriminant >= 0) { + CGFloat sum = b + std::copysign(std::sqrt(discriminant), b); + CGFloat q = -0.5 * sum; + CGFloat first = a != 0 ? q / a : -1; + CGFloat second = q != 0 ? (-2 * c) / sum : -1; + if (second < first) { + std::swap(first, second); + } + if (first < 0.0001) { + first = second; + second = 2; + } + if (first >= 0.0001 && first <= 0.9999) { + Cubic left, right; + curve.split(first, left, right); + cubic(left, axis + 1); + curve = right; + if (1 - second >= 0.0001) { + curve.split((second - first) / (1 - first), left, right); + cubic(left, axis + 1); + curve = right; + } + } + } + cubic(curve, axis + 1); + return; + } + CGPoint offset = begin(curve.p[3]); + CGPathAddCurveToPoint(path, nullptr, curve.p[1].x + offset.x, curve.p[1].y + offset.y, + curve.p[2].x + offset.x, curve.p[2].y + offset.y, + curve.p[3].x + offset.x, curve.p[3].y + offset.y); + current = curve.p[3]; + } +}; + +struct DilationInput { + struct Part { + CGPathElementType type; + CGPoint points[4]; + }; + struct Subpath { + std::vector parts; + }; + std::vector subpaths; + CGPoint current = CGPointZero; + CGPoint start = CGPointZero; + CGFloat orientation = 0; + + void close_orientation() { + orientation += current.x * start.y - current.y * start.x; + current = start; + } + + void append(const CGPathElement &element) { + if (element.type == kCGPathElementMoveToPoint) { + close_orientation(); + current = start = element.points[0]; + subpaths.emplace_back(); + return; + } + if (element.type == kCGPathElementCloseSubpath) { + close_orientation(); + subpaths.emplace_back(); + return; + } + if (subpaths.empty()) { + subpaths.emplace_back(); + } + unsigned count = element.type == kCGPathElementAddCurveToPoint ? 3 : element.type == kCGPathElementAddQuadCurveToPoint ? 2 : 1; + Part part{element.type, {current}}; + std::copy_n(element.points, count, part.points + 1); + for (unsigned i = 0; i < count; ++i) { + orientation += current.x * element.points[i].y - current.y * element.points[i].x; + current = element.points[i]; + } + subpaths.back().parts.push_back(part); + } +}; + +CGPathRef copy_dilated_path(CGPathRef path, CGPoint amount) { + DilationInput input; + CGPathApply(path, &input, +[](void *context, const CGPathElement *element) { + static_cast(context)->append(*element); + }); + input.close_orientation(); + CGMutablePathRef result = CGPathCreateMutable(); + Dilator dilator{result, amount}; + bool reverse = input.orientation < 0; + for (auto &subpath : input.subpaths) { + if (subpath.parts.empty()) { + continue; + } + if (reverse) { + std::reverse(subpath.parts.begin(), subpath.parts.end()); + for (auto &part : subpath.parts) { + unsigned count = part.type == kCGPathElementAddCurveToPoint ? 3 : part.type == kCGPathElementAddQuadCurveToPoint ? 2 : 1; + std::reverse(part.points, part.points + count + 1); + } + } + dilator.move(subpath.parts[0].points[0]); + for (const auto &part : subpath.parts) { + switch (part.type) { + case kCGPathElementAddLineToPoint: dilator.line(part.points[1]); break; + case kCGPathElementAddQuadCurveToPoint: dilator.quad(part.points[1], part.points[2]); break; + case kCGPathElementAddCurveToPoint: dilator.cubic({{part.points[0], part.points[1], part.points[2], part.points[3]}}); break; + default: break; + } + } + dilator.close(); + } + return result; +} + +CGPoint add(CGPoint a, CGPoint b) { return {a.x + b.x, a.y + b.y}; } +CGPoint subtract(CGPoint a, CGPoint b) { return {a.x - b.x, a.y - b.y}; } +CGPoint scale(CGPoint a, CGFloat b) { return {a.x * b, a.y * b}; } +CGFloat dot(CGPoint a, CGPoint b) { return std::fma(a.y, b.y, a.x * b.x); } +CGFloat cross(CGPoint a, CGPoint b) { return std::fma(a.x, b.y, -a.y * b.x); } +CGPoint normalize(CGPoint a) { + if (a.x == 0 && a.y == 0) { return a; } + CGFloat length = std::sqrt(dot(a, a)); + return {a.x / length, a.y / length}; +} +CGPoint multiply_add(CGPoint a, CGFloat b, CGPoint c) { + return {std::fma(a.x, b, c.x), std::fma(a.y, b, c.y)}; +} + +struct OffsetCubic { + Cubic curve; + CGPoint linear, quadratic, cubic; + + explicit OffsetCubic(Cubic curve) : curve(curve) { + CGPoint a = subtract(curve.p[1], curve.p[0]); + CGPoint b = subtract(curve.p[2], curve.p[1]); + CGPoint c = subtract(curve.p[3], curve.p[2]); + linear = scale(a, 3); + quadratic = scale(subtract(b, a), 3); + cubic = add(subtract(c, scale(b, 2)), a); + } + + CGPoint evaluate(CGFloat t) const { + return multiply_add(multiply_add(multiply_add(cubic, t, quadratic), t, linear), t, curve.p[0]); + } + + CGPoint derivative(CGFloat t) const { + if (t == 0 || t == 1) { + for (unsigned i = 1; i < 4; ++i) { + CGPoint tangent = t == 0 ? subtract(curve.p[i], curve.p[0]) : subtract(curve.p[3], curve.p[3 - i]); + if (dot(tangent, tangent) > 1e-8 || i == 3) { return tangent; } + } + } + return multiply_add(multiply_add(cubic, 3 * t, scale(quadratic, 2)), t, linear); + } + + bool near(CGPoint point, CGFloat t) const { + auto squared_distance = [&](CGFloat parameter) { + // Subtract first to match the polynomial evaluator's rounding. + CGPoint v = multiply_add(multiply_add(multiply_add(cubic, parameter, quadratic), parameter, linear), parameter, subtract(curve.p[0], point)); + return dot(v, v); + }; + constexpr CGFloat toleranceSquared = 0.1 * 0.1; + CGFloat first = squared_distance(0), last = squared_distance(1), middle = squared_distance(t); + if (first < toleranceSquared || last < toleranceSquared || middle < toleranceSquared) { return true; } + if (!(middle < first && middle < last)) { return false; } + constexpr CGFloat ratio = 0.6180339887498949; + CGFloat left = 0, right = 1; + CGFloat x = t > 0.5 ? t * ratio : t; + CGFloat y = t > 0.5 ? t : std::fma(1 - ratio, 1 - t, t); + CGFloat fx = x == t ? middle : squared_distance(x); + CGFloat fy = y == t ? middle : squared_distance(y); + if (fx < toleranceSquared || fy < toleranceSquared) { return true; } + while (right - left > 3e-8 * (x + y)) { + if (fy < fx) { + left = x; + x = std::fma(ratio, y - right, right); + fx = squared_distance(x); + if (fx <= toleranceSquared) { return true; } + } else { + right = y; + y = std::fma(ratio, x - left, left); + fy = squared_distance(y); + if (fy <= toleranceSquared) { return true; } + } + std::swap(x, y); + std::swap(fx, fy); + } + return false; + } +}; + +struct Offsetter { + CGMutablePathRef path; + CGFloat amount; + CGPoint start = CGPointZero; + CGPoint current = CGPointZero; + CGPoint firstDirection = CGPointZero; + CGPoint lastDirection = CGPointZero; + unsigned state = 0; + + CGPoint normal(CGPoint tangent) const { + CGPoint unit = normalize(tangent); + return {-unit.y * amount, unit.x * amount}; + } + + void emit_line(CGPoint p) { CGPathAddLineToPoint(path, nullptr, p.x, p.y); } + void emit_cubic(Cubic c) { + CGPathAddCurveToPoint(path, nullptr, c.p[1].x, c.p[1].y, c.p[2].x, c.p[2].y, c.p[3].x, c.p[3].y); + } + + void join(CGPoint point, CGPoint direction) { + if (dot(lastDirection, direction) > 0.99) { return; } + CGPoint oldNormal{-lastDirection.y * amount, lastDirection.x * amount}; + CGPoint newNormal{-direction.y * amount, direction.x * amount}; + CGPoint delta = subtract(oldNormal, newNormal); + CGPoint end = add(point, newNormal); + if (dot(delta, delta) < 0.05) { emit_line(end); return; } + CGPoint sum = add(lastDirection, direction); + CGFloat length = std::sqrt(dot(sum, sum)); + CGFloat factor = -4.0 / 3.0; + if (length >= 0.001) { + CGFloat dx = lastDirection.x - direction.x, dy = lastDirection.y - direction.y; + if (std::abs(dy) > std::abs(dx)) { + factor = ((8.0 / 3.0) / length - 4.0 / 3.0) * sum.x / dy; + } else { + factor = (4.0 / 3.0 - (8.0 / 3.0) / length) * sum.y / dx; + } + } + CGPoint control1 = multiply_add(lastDirection, amount * factor, add(point, oldNormal)); + CGPoint control2 = multiply_add(direction, -amount * factor, end); + emit_cubic({{CGPointZero, control1, control2, end}}); + } + + void begin(CGPoint point, CGPoint tangent) { + CGPoint direction = normalize(tangent); + if (state == 4) { + join(point, direction); + } else { + state = 4; + firstDirection = direction; + CGPoint p = add(point, normal(tangent)); + CGPathMoveToPoint(path, nullptr, p.x, p.y); + } + lastDirection = direction; + } + + bool line_segment(CGPoint a, CGPoint b) { + CGPoint tangent = subtract(b, a); + if (!(dot(tangent, tangent) > 1e-8)) { + if (state == 2) { state = 3; } + return false; + } + begin(a, tangent); + emit_line(add(b, normal(tangent))); + return true; + } + + void fit(Cubic curve, int depth) { + OffsetCubic source(curve); + CGPoint firstTangent = source.derivative(0), lastTangent = source.derivative(1); + CGPoint first = add(curve.p[0], normal(firstTangent)); + CGPoint last = add(curve.p[3], normal(lastTangent)); + CGPoint chord = subtract(curve.p[3], curve.p[0]); + if (dot(chord, chord) <= 1e-8) { emit_line(last); return; } + CGPoint middleTangent = source.derivative(0.5); + CGPoint middle = add(source.evaluate(0.5), normal(middleTangent)); + CGPoint adjusted = multiply_add(subtract(scale(middle, 2), add(first, last)), 1.0 / 6.0, middle); + auto intersection = [&](CGPoint point, CGPoint tangent) { + CGFloat determinant = cross(tangent, middleTangent); + if (std::abs(determinant) < 0.0001) { return adjusted; } + CGFloat t = cross(subtract(adjusted, point), middleTangent) / determinant; + return multiply_add(tangent, t, point); + }; + Cubic result{{first, intersection(first, firstTangent), intersection(last, lastTangent), last}}; + if (--depth >= 0) { + OffsetCubic candidate(result); + for (CGFloat t : {0.5, 0.3, 0.7, 0.1, 0.9}) { + CGPoint expected = t == 0.5 ? middle : add(source.evaluate(t), normal(source.derivative(t))); + bool finite = std::isfinite(expected.x) && std::isfinite(expected.y); + for (auto p : result.p) { finite = finite && std::isfinite(p.x) && std::isfinite(p.y); } + if (finite && !candidate.near(expected, t)) { + Cubic left, right; + curve.split(0.5, left, right); + fit(left, depth); + fit(right, depth); + return; + } + } + } + emit_cubic(result); + } + + void offset_curve(Cubic curve) { + CGPoint a = subtract(curve.p[1], curve.p[0]), b = subtract(curve.p[2], curve.p[1]); + CGPoint c = subtract(curve.p[3], curve.p[2]); + if (dot(a, b) >= 0 && dot(b, c) >= 0) { fit(curve, 4); return; } + if (dot(a, a) <= 1e-8 && dot(b, b) <= 1e-8 && dot(c, c) <= 1e-8) { + emit_line(add(curve.p[3], normal(OffsetCubic(curve).derivative(1)))); + return; + } + Cubic left, right; + curve.split(0.5, left, right); + auto equal = [](const Cubic &a, const Cubic &b) { + for (unsigned i = 0; i < 4; ++i) { + if (!(a.p[i].x == b.p[i].x || (std::isnan(a.p[i].x) && std::isnan(b.p[i].x))) || + !(a.p[i].y == b.p[i].y || (std::isnan(a.p[i].y) && std::isnan(b.p[i].y)))) { return false; } + } + return true; + }; + if (!equal(curve, left)) { offset_curve(left); } + if (!equal(curve, right)) { offset_curve(right); } + } + + bool cubic_segment(Cubic curve) { + CGPoint a = subtract(curve.p[1], curve.p[0]), b = subtract(curve.p[2], curve.p[1]); + CGPoint c = subtract(curve.p[3], curve.p[2]); + if (dot(a, a) <= 1e-8 && dot(b, b) <= 1e-8 && dot(c, c) <= 1e-8) { + if (state == 2) { state = 3; } + return false; + } + OffsetCubic source(curve); + begin(curve.p[0], source.derivative(0)); + lastDirection = normalize(source.derivative(1)); + if (cross(a, b) * cross(b, c) <= 0) { + CGFloat roots[2] = {-1, -1}; + for (unsigned axis = 0; axis < 2; ++axis) { + CGFloat quadratic = axis == 0 ? source.quadratic.x : source.quadratic.y; + CGFloat cubic = axis == 0 ? source.cubic.x : source.cubic.y; + CGFloat t = -quadratic / (3 * cubic); + if (cubic != 0 && t > 0 && t < 1) { roots[axis] = t; } + } + if (roots[0] == -1) { std::swap(roots[0], roots[1]); } + if (roots[1] != -1) { + if (std::abs(roots[0] - roots[1]) < 1e-6) { roots[1] = -1; } + else if (roots[1] < roots[0]) { std::swap(roots[0], roots[1]); } + } + if (roots[0] != -1) { + Cubic left, right; + curve.split(roots[0], left, right); + offset_curve(left); + curve = right; + if (roots[1] != -1) { + curve.split((roots[1] - roots[0]) / (1 - roots[0]), left, right); + offset_curve(left); + curve = right; + } + } + } + offset_curve(curve); + return true; + } + + void apply(const CGPathElement &element) { + switch (element.type) { + case kCGPathElementMoveToPoint: + start = current = element.points[0]; state = 2; break; + case kCGPathElementAddLineToPoint: + if (state && line_segment(current, element.points[0])) { current = element.points[0]; } + break; + case kCGPathElementAddQuadCurveToPoint: { + CGPoint control = element.points[0], end = element.points[1]; + Cubic curve{{current, multiply_add(subtract(current, control), 1.0 / 3.0, control), + multiply_add(subtract(end, control), 1.0 / 3.0, control), end}}; + if (state && cubic_segment(curve)) { current = end; } + break; + } + case kCGPathElementAddCurveToPoint: + if (state && cubic_segment({{current, element.points[0], element.points[1], element.points[2]}})) { current = element.points[2]; } + break; + case kCGPathElementCloseSubpath: + if (state >= 2) { + if (state == 4) { + line_segment(current, start); + join(start, firstDirection); + CGPathCloseSubpath(path); + } + state = 1; current = start; + } + break; + } + } +}; + +CGPathRef copy_offset_path(CGPathRef path, CGFloat amount) { + CGMutablePathRef result = CGPathCreateMutable(); + Offsetter offsetter{result, amount}; + CGPathApply(path, &offsetter, +[](void *context, const CGPathElement *element) { + static_cast(context)->apply(*element); + }); + return result; +} + +struct Frame { + unsigned type; + CGMutablePathRef path = CGPathCreateMutable(); + CGPathRef accumulated = nullptr; + CGAffineTransform transform = CGAffineTransformIdentity; + CGFloat values[3] = {}; + int32_t cap = 0; + int32_t join = 0; + uint8_t operation = 0; + bool evenOdd = false; + size_t emittedElements = 0; + std::vector dash; + uint32_t clipFlags = 0; + std::vector clipKeys; + + explicit Frame(unsigned type) : type(type) {} + ~Frame() { + CGPathRelease(path); + CGPathRelease(accumulated); + } + + void end_boolean() { + bool hasPath = !CGPathIsEmpty(path); + if (!accumulated) { + if (operation != 0 && hasPath) { + accumulated = CGPathCreateCopy(path); + CGPathRelease(path); + path = CGPathCreateMutable(); + } + return; + } + CGPathRef next = nullptr; + switch (operation) { + case 1: next = hasPath ? CGPathCreateCopyByIntersectingPath(accumulated, path, evenOdd) : nullptr; break; + case 2: next = hasPath ? CGPathCreateCopyByUnioningPath(accumulated, path, evenOdd) : CGPathRetain(accumulated); break; + case 3: next = hasPath ? CGPathCreateCopyBySubtractingPath(accumulated, path, evenOdd) : CGPathRetain(accumulated); break; + case 4: next = hasPath ? CGPathCreateCopyBySymmetricDifferenceOfPath(accumulated, path, evenOdd) : CGPathRetain(accumulated); break; + case 5: next = hasPath ? CGPathCreateCopyOfLineByIntersectingPath(accumulated, path, evenOdd) : nullptr; break; + case 6: next = hasPath ? CGPathCreateCopyOfLineBySubtractingPath(accumulated, path, evenOdd) : nullptr; break; + default: return; + } + CGPathRelease(accumulated); + accumulated = next; + } + + CGPathRef finish() { + switch (type) { + case 17: + return CGPathCreateCopyByTransformingPath(path, &transform); + case 18: + return copy_trimmed_path(path, static_cast(values[0]), static_cast(values[1])); + case 19: { + CGPathRef input = dash.empty() ? CGPathRetain(path) + : CGPathCreateCopyByDashingPath(path, nullptr, static_cast(values[2]), dash.data(), dash.size()); + CGPathRef result = CGPathCreateCopyByStrokingPath(input, nullptr, static_cast(values[0]), static_cast(cap), static_cast(join), static_cast(values[1])); + CGPathRelease(input); + return result; + } + case 20: + end_boolean(); + return operation == 0 ? CGPathCreateCopyByNormalizing(path, evenOdd) : CGPathRetain(accumulated); + case 22: + return copy_dilated_path(path, CGPointMake(values[0], values[1])); + case 23: + return copy_offset_path(path, values[0]); + case 24: + return copy_clip_stroke_path(path, float(values[0]), {clipFlags, cap, clipKeys.size(), clipKeys.data()}); + default: + return CGPathRetain(path); + } + } +}; + +void append_element(CGMutablePathRef path, const CGPathElement &element) { + const CGPoint *p = element.points; + switch (element.type) { + case kCGPathElementMoveToPoint: CGPathMoveToPoint(path, nullptr, p[0].x, p[0].y); break; + case kCGPathElementAddLineToPoint: CGPathAddLineToPoint(path, nullptr, p[0].x, p[0].y); break; + case kCGPathElementAddQuadCurveToPoint: CGPathAddQuadCurveToPoint(path, nullptr, p[0].x, p[0].y, p[1].x, p[1].y); break; + case kCGPathElementAddCurveToPoint: CGPathAddCurveToPoint(path, nullptr, p[0].x, p[0].y, p[1].x, p[1].y, p[2].x, p[2].y); break; + case kCGPathElementCloseSubpath: CGPathCloseSubpath(path); break; + } +} + +struct Mapper { + std::vector> frames; + void *outputInfo = nullptr; + ORBPathApplierCallback output = nullptr; + bool active = true; + bool open = false; + + bool forwards(size_t index) const { + while (index) { + if (frames[index--]->type != 17) { return false; } + } + return output != nullptr; + } + + void emit(size_t index, const CGPathElement &element, const CGAffineTransform *transform = nullptr) { + if (!active || !forwards(index)) { return; } + if (element.type == kCGPathElementCloseSubpath) { + if (!open) { return; } + open = false; + } else { + open = true; + } + static constexpr size_t counts[] = {1, 1, 2, 3, 0}; + size_t count = counts[element.type]; + CGPoint points[3]; + std::copy_n(element.points, count, points); + auto applyTransform = [&](const CGAffineTransform &t) { + for (size_t i = 0; i < count; ++i) { + CGPoint p = points[i]; + points[i] = {std::fma(t.c, p.y, std::fma(t.a, p.x, t.tx)), + std::fma(t.d, p.y, std::fma(t.b, p.x, t.ty))}; + } + }; + if (transform) { applyTransform(*transform); } + while (index) { applyTransform(frames[index--]->transform); } + CGPathElement mapped{element.type, count ? points : nullptr}; + active = output(outputInfo, &mapped); + } + + Mapper() { frames.push_back(std::make_unique(0)); } + + Frame ¤t() { return *frames.back(); } + + bool flush(size_t index) { + Frame &frame = *frames[index]; + CGPathRef path = frame.finish(); + if (!path) { return false; } + struct AppendInfo { + Mapper *mapper; + size_t index; + bool emit; + size_t skip; + size_t count = 0; + } info{this, index - 1, frame.type != 17, frame.type == 23 ? frame.emittedElements : 0}; + CGPathApply(path, &info, +[](void *context, const CGPathElement *element) { + auto &info = *static_cast(context); + if (info.count++ < info.skip) { return; } + append_element(info.mapper->frames[info.index]->path, *element); + if (info.emit) { info.mapper->emit(info.index, *element); } + }); + frame.emittedElements = info.count; + CGPathRelease(path); + return true; + } + + void end_path(size_t index) { + Frame &frame = *frames[index]; + switch (frame.type) { + case 20: + frame.end_boolean(); + break; + case 18: case 22: case 24: + flush(index); + break; + case 19: + flush(index); + CGPathRelease(frame.path); + frame.path = CGPathCreateMutable(); + end_path(index - 1); + break; + case 23: + // Offset endpath forwards the boundary without resetting segment state. + flush(index); + end_path(index - 1); + break; + default: + break; + } + } + + void pop() { + if (frames.size() == 1) { + return; + } + unsigned type = current().type; + bool hasResult = flush(frames.size() - 1); + frames.pop_back(); + if (type == 19 || type == 23 || (type == 20 && hasResult)) { + end_path(frames.size() - 1); + } + } + + bool apply(ORBPathElement element, const CGFloat *values, const void *info) { + auto type = static_cast(element); + auto *transform = static_cast(info); + size_t index = frames.size() - 1; + CGMutablePathRef path = current().path; + CGMutablePathRef primitive = nullptr; + bool skipMove = false; + if (type < 5) { + CGPoint points[3]; + size_t count = Storage::argument_count(element) / 2; + for (size_t i = 0; i < count; ++i) { points[i] = {values[2 * i], values[2 * i + 1]}; } + CGPathElement mapped{static_cast(type), count ? points : nullptr}; + emit(index, mapped, type == 4 ? nullptr : transform); + } else if (type <= 15 && forwards(index)) { + primitive = CGPathCreateMutable(); + if (type >= 12 && !CGPathIsEmpty(path)) { + CGPoint point = CGPathGetCurrentPoint(path); + CGPathMoveToPoint(primitive, nullptr, point.x, point.y); + skipMove = true; + } + path = primitive; + } + switch (type) { + case 0: + CGPathMoveToPoint(path, transform, values[0], values[1]); + break; + case 1: + CGPathAddLineToPoint(path, transform, values[0], values[1]); + break; + case 2: + CGPathAddQuadCurveToPoint(path, transform, values[0], values[1], values[2], values[3]); + break; + case 3: + CGPathAddCurveToPoint(path, transform, values[0], values[1], values[2], values[3], values[4], values[5]); + break; + case 4: + CGPathCloseSubpath(path); + break; + case 5: + CGPathAddRect(path, transform, CGRectMake(values[0], values[1], values[2], values[3])); + break; + case 6: { + CGRect rect = CGRectStandardize(CGRectMake(values[0], values[1], values[2], values[3])); + CGFloat left = CGRectGetMinX(rect), right = CGRectGetMaxX(rect); + CGFloat top = CGRectGetMinY(rect), bottom = CGRectGetMaxY(rect); + CGPathMoveToPoint(path, transform, right, CGRectGetMidY(rect)); + CGPathAddLineToPoint(path, transform, right, bottom); + CGPathAddLineToPoint(path, transform, left, bottom); + CGPathAddLineToPoint(path, transform, left, top); + CGPathAddLineToPoint(path, transform, right, top); + CGPathCloseSubpath(path); + break; + } + case 7: + CGPathAddEllipseInRect(path, transform, CGRectMake(values[0], values[1], values[2], values[3])); + break; + case 8: case 9: case 10: case 11: + add_rounded_rect(path, element, values, transform); + break; + case 12: + add_relative_arc(path, transform, {values[0], values[1]}, values[2], values[3], values[4]); + break; + case 13: case 14: + add_arc(path, transform, {values[0], values[1]}, values[2], values[3], values[4], type == 13); + break; + case 15: + CGPathAddArcToPoint(path, transform, values[0], values[1], values[2], values[3], values[4]); + break; + case 16: + pop(); + break; + case 17: case 18: case 19: case 20: case 22: case 23: case 24: { + auto frame = std::make_unique(type); + if (type == 17 && info) { + frame->transform = *transform; + } else if (type == 24) { + frame->values[0] = values[0]; + if (info) { + const auto &clip = *static_cast(info); + frame->clipFlags = clip.flags; + frame->cap = clip.cap; + if (clip.count) frame->clipKeys.assign(clip.keys, clip.keys + clip.count); + } + } else if (type == 23) { + frame->values[0] = values[0]; + } else if (type == 18 || type == 22) { + frame->values[0] = values[0]; + frame->values[1] = values[1]; + } else if (type == 19) { + std::copy_n(values, 3, frame->values); + if (info) { + const auto &stroke = *static_cast(info); + frame->cap = stroke.cap; + frame->join = stroke.join; + if (stroke.count) { + auto *dash = static_cast(stroke.values); + frame->dash.assign(dash, dash + stroke.count); + } + } + } else if (type == 20 && info) { + auto *flags = static_cast(info); + frame->operation = flags[0]; + frame->evenOdd = (flags[1] & 1) != 0; + } + frames.push_back(std::move(frame)); + break; + } + case 21: + end_path(frames.size() - 1); + break; + default: + precondition_failure("unsupported path mapping element: %u", type); + } + if (primitive) { + struct ReplayInfo { + Mapper *mapper; + size_t index; + bool skipMove; + } replay{this, index, skipMove}; + CGPathApply(primitive, &replay, +[](void *context, const CGPathElement *element) { + auto &replay = *static_cast(context); + if (replay.skipMove) { replay.skipMove = false; return; } + append_element(replay.mapper->frames[replay.index]->path, *element); + replay.mapper->emit(replay.index, *element); + }); + CGPathRelease(primitive); + } + return true; + } +}; + +} // namespace + +bool apply_mapping_element(void *context, ORBPathElement element, const CGFloat *values, const void *info) { + return static_cast(context)->apply(element, values, info); +} + +namespace { +struct MapCache { + std::mutex mutex; + std::map, std::shared_ptr> entries; +}; + +MapCache &map_cache() { + static auto *cache = new MapCache; + return *cache; +} +} // namespace + +void invalidate_mapped_storage(uint32_t identifier) { + auto &cache = map_cache(); + std::lock_guard guard(cache.mutex); + auto first = cache.entries.lower_bound({identifier, 0}); + while (first != cache.entries.end() && first->first.first == identifier) { + first = cache.entries.erase(first); + } +} + +bool apply_mapped_storage(const Storage &storage, void *context, size_t depth) { + auto &cache = map_cache(); + auto key = std::make_pair(storage.identifier(), depth); + std::shared_ptr mapped; + { + std::lock_guard guard(cache.mutex); + auto entry = cache.entries.find(key); + if (entry != cache.entries.end()) { mapped = entry->second; } + } + if (!mapped) { + Mapper mapper; + bool complete = storage.apply_elements_at_depth(&mapper, + +[](void *context, ORBPathElement element, const CGFloat *values, const void *info) { + return static_cast(context)->apply(element, values, info); + }, depth); + if (!complete) { return false; } + while (mapper.frames.size() > 1) { mapper.pop(); } + mapped = std::make_shared(sizeof(Storage)); + CGPathApply(mapper.current().path, mapped.get(), +[](void *context, const CGPathElement *element) { + static_cast(context)->append_element(static_cast(element->type), + reinterpret_cast(element->points), nullptr); + }); + std::lock_guard guard(cache.mutex); + mapped = cache.entries.emplace(key, mapped).first->second; + } + return mapped->apply_elements(context, apply_mapping_element); +} + +bool apply_mapped_path(ORBPath path, void *info, ORBPathApplierCallback callback) { + Mapper mapper; + mapper.outputInfo = info; + mapper.output = callback; + bool complete = ORBPathApplyElements(path, &mapper, apply_mapping_element); + if (!complete) { precondition_failure("path enumeration stopped during conversion"); } + while (mapper.frames.size() > 1) { mapper.pop(); } + return mapper.active; +} + +CGPathRef copy_cg_path(ORBPath path) { + Mapper mapper; + bool complete = ORBPathApplyElements(path, &mapper, apply_mapping_element); + if (!complete) { + precondition_failure("path enumeration stopped during conversion"); + } + while (mapper.frames.size() > 1) { + mapper.pop(); + } + return CGPathRetain(mapper.current().path); +} + +} /* Path */ +} /* ORB */ + +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/PathMapper.hpp b/Sources/OpenRenderBoxCxx/Path/PathMapper.hpp new file mode 100644 index 0000000..152cf78 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/PathMapper.hpp @@ -0,0 +1,24 @@ +// +// PathMapper.hpp +// OpenRenderBox + +#pragma once + +#include + +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN + +#include + +namespace ORB { +namespace Path { +class Storage; +CGPathRef copy_cg_path(ORBPath path); +bool apply_mapped_path(ORBPath path, void *info, ORBPathApplierCallback callback); +bool apply_mapping_element(void *context, ORBPathElement element, const CGFloat *values, const void *info); +bool apply_mapped_storage(const Storage &storage, void *context, size_t depth); +void invalidate_mapped_storage(uint32_t identifier); +} /* Path */ +} /* ORB */ + +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/PathStorageObject.cpp b/Sources/OpenRenderBoxCxx/Path/PathStorageObject.cpp new file mode 100644 index 0000000..409f2dc --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/PathStorageObject.cpp @@ -0,0 +1,68 @@ +// +// PathStorageObject.cpp +// OpenRenderBox + +#include "PathStorageObject.hpp" + +#if OPENRENDERBOX_CF_CGTYPES + +#include "NestedCallbacks.hpp" +#include "Storage.hpp" +#include + +namespace ORB::Path { +namespace { + +struct StorageObject { + mutable std::atomic_uint references{1}; + Storage storage; + + explicit StorageObject(const Storage &source) : storage(sizeof(Storage), source) {} +}; + +const ORBPathCallbacks callbacks = { + .flags = {}, + .retain = +[](const void *object) -> const void * { + static_cast(object)->references.fetch_add(1, std::memory_order_relaxed); + return object; + }, + .release = +[](const void *object) { + auto *value = static_cast(object); + if (value->references.fetch_sub(1, std::memory_order_acq_rel) == 1) delete value; + }, + .apply = +[](const void *object, void *info, ORBPathApplyCallback callback) { + return static_cast(object)->storage.apply_elements(info, callback); + }, + .isEqual = +[](const void *a, const void *b) { + return static_cast(a)->storage == static_cast(b)->storage; + }, + .isEmpty = +[](const void *object) { return static_cast(object)->storage.isEmpty(); }, + .isSingleElement = +[](const void *object) { + return static_cast(object)->storage.flags().isSingleElement(); + }, + .bezierOrder = +[](const void *object) -> size_t { + return static_cast(object)->storage.flags().bezierOrder(); + }, + #if ORB_TARGET_OS_DARWIN + .boundingRect = +[](const void *object) { return static_cast(object)->storage.bounding_rect(); }, + .cgPath = +[](const void *object) { return static_cast(object)->storage.cgpath(); }, + #else + .boundingRect = nullptr, + .cgPath = nullptr, + #endif + .next = +[](const void *object) -> ORBPath { + auto *nested = NestedCallbacks::get(&callbacks, 1); + return {reinterpret_cast(const_cast(object)), + reinterpret_cast(&nested->extended)}; + }, +}; + +} // namespace + +ORBPath copy_storage_path(const Storage &storage) { + return {reinterpret_cast(new StorageObject(storage)), &callbacks}; +} + +} // namespace ORB::Path + +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/PathStorageObject.hpp b/Sources/OpenRenderBoxCxx/Path/PathStorageObject.hpp new file mode 100644 index 0000000..5e61de3 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/PathStorageObject.hpp @@ -0,0 +1,18 @@ +// +// PathStorageObject.hpp +// OpenRenderBox + +#pragma once + +#include + +#if OPENRENDERBOX_CF_CGTYPES + +#include + +namespace ORB::Path { +class Storage; +ORBPath copy_storage_path(const Storage &storage); +} + +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/RoundedRect.cpp b/Sources/OpenRenderBoxCxx/Path/RoundedRect.cpp new file mode 100644 index 0000000..2a01899 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/RoundedRect.cpp @@ -0,0 +1,109 @@ +#include +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN +#include "RoundedRect.hpp" +#include +#include + +namespace ORB::Path { +namespace { +void constrain_pair(CGFloat &a, CGFloat &b, CGFloat length) { + CGFloat deficit = length - (a + b); + if (deficit < 0 && a != b) { + CGFloat reduction = std::min(-deficit, std::abs(a - b)); + (a > b ? a : b) -= reduction; + deficit += reduction; + } + if (deficit < 0) { + CGFloat delta = a * deficit / (a + b); + a += delta; + b += deficit - delta; + } +} + +struct EdgeCoefficients { + CGFloat start = static_cast(1.528665f); + CGFloat control1 = static_cast(1.08849f); + CGFloat control2 = static_cast(0.868407f); +}; + +EdgeCoefficients coefficients(CGFloat length, CGFloat a, CGFloat b) { + float sum = static_cast(a) + static_cast(b); + float ratio = (static_cast(std::abs(length)) - sum) / (sum * 0.528664947f); + if (!(ratio < 1)) { return {}; } + float t = std::max(0.0f, std::min(1.0f, ratio)); + return {std::fma(t, 0.528664947f, 1.0f), std::fma(t, 0.128490031f, 0.96f), std::fma(t, 0.0484070182f, 0.82f)}; +} +} // namespace + +void add_rounded_rect(CGMutablePathRef path, ORBPathElement element, const CGFloat *values, const CGAffineTransform *transform) { + CGRect rect = CGRectStandardize(CGRectMake(values[0], values[1], values[2], values[3])); + CGFloat w = rect.size.width, h = rect.size.height; + CGFloat rx[4], ry[4]; + if (element == ORBPathElementFixedRoundedRectCircular || element == ORBPathElementFixedRoundedRectContinuous) { + CGFloat limit = std::min(w, h) * 0.5; + std::fill(rx, rx + 4, std::min(limit, std::max(CGFloat(0), values[4]))); + std::fill(ry, ry + 4, std::min(limit, std::max(CGFloat(0), values[5]))); + } else { + CGFloat limit = std::max(w, h); + for (size_t i = 0; i != 4; ++i) { rx[i] = std::min(limit, std::max(CGFloat(0), values[4 + i])); } + constrain_pair(rx[0], rx[1], w); + constrain_pair(rx[3], rx[2], w); + constrain_pair(rx[0], rx[3], h); + constrain_pair(rx[1], rx[2], h); + std::copy(rx, rx + 4, ry); + } + CGFloat x0 = CGRectGetMinX(rect), y0 = CGRectGetMinY(rect); + CGFloat x1 = CGRectGetMaxX(rect), y1 = CGRectGetMaxY(rect); + bool emptyRadii = true; + for (size_t i = 0; i != 4; ++i) { emptyRadii &= rx[i] == 0 && ry[i] == 0; } + CGPathMoveToPoint(path, transform, x1, (y0 + ry[1] + y1 - ry[2]) * 0.5); + if (emptyRadii) { + CGPathAddLineToPoint(path, transform, x1, y1); + CGPathAddLineToPoint(path, transform, x0, y1); + CGPathAddLineToPoint(path, transform, x0, y0); + CGPathAddLineToPoint(path, transform, x1, y0); + CGPathCloseSubpath(path); + return; + } + bool continuous = element == ORBPathElementFixedRoundedRectContinuous || element == ORBPathElementUnevenRoundedRectContinuous; + // Each corner starts on its incoming edge and ends on the next edge. + const CGPoint origins[4] = {{x0, y0}, {x1, y0}, {x1, y1}, {x0, y1}}; + const CGPoint incoming[4] = {{0, 1}, {-1, 0}, {0, -1}, {1, 0}}; + const CGPoint outgoing[4] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; + EdgeCoefficients edges[4] = { + coefficients(w, rx[0], rx[1]), coefficients(h, ry[1], ry[2]), + coefficients(w, rx[2], rx[3]), coefficients(h, ry[3], ry[0]) + }; + for (size_t turn = 0; turn != 4; ++turn) { + size_t c = (turn + 2) % 4; + CGFloat a = incoming[c].x == 0 ? ry[c] : rx[c]; + CGFloat b = outgoing[c].x == 0 ? ry[c] : rx[c]; + auto point = [&](CGFloat u, CGFloat v) -> CGPoint { + return {origins[c].x + incoming[c].x * (u * a) + outgoing[c].x * (v * b), + origins[c].y + incoming[c].y * (u * a) + outgoing[c].y * (v * b)}; + }; + auto line = [&](CGFloat u, CGFloat v) { auto p = point(u, v); CGPathAddLineToPoint(path, transform, p.x, p.y); }; + auto curve = [&](CGFloat u1, CGFloat v1, CGFloat u2, CGFloat v2, CGFloat u3, CGFloat v3) { + auto p = point(u1, v1), q = point(u2, v2), r = point(u3, v3); + CGPathAddCurveToPoint(path, transform, p.x, p.y, q.x, q.y, r.x, r.y); + }; + if (continuous) { + auto before = edges[(c + 3) % 4], after = edges[c]; + constexpr CGFloat small = static_cast(0.0749114f); + constexpr CGFloat large = static_cast(0.631494f); + constexpr CGFloat middle1 = static_cast(0.16906f); + constexpr CGFloat middle2 = static_cast(0.372824f); + line(before.start, 0); + curve(before.control1, 0, before.control2, 0, large, small); + curve(middle2, middle1, middle1, middle2, small, large); + curve(0, after.control2, 0, after.control1, 0, after.start); + } else { + constexpr CGFloat k = 1 - 0.5522847498; + line(1, 0); + curve(k, 0, 0, k, 0, 1); + } + } + CGPathCloseSubpath(path); +} +} // namespace ORB::Path +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/RoundedRect.hpp b/Sources/OpenRenderBoxCxx/Path/RoundedRect.hpp new file mode 100644 index 0000000..e0b4e08 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Path/RoundedRect.hpp @@ -0,0 +1,9 @@ +#pragma once + +#if OPENRENDERBOX_CF_CGTYPES && ORB_TARGET_OS_DARWIN +#include + +namespace ORB::Path { +void add_rounded_rect(CGMutablePathRef path, ORBPathElement element, const CGFloat *values, const CGAffineTransform *transform); +} +#endif diff --git a/Sources/OpenRenderBoxCxx/Path/Storage.cpp b/Sources/OpenRenderBoxCxx/Path/Storage.cpp index 30ad2ab..b4155bd 100644 --- a/Sources/OpenRenderBoxCxx/Path/Storage.cpp +++ b/Sources/OpenRenderBoxCxx/Path/Storage.cpp @@ -4,151 +4,546 @@ #if OPENRENDERBOX_CF_CGTYPES -#include #include +#include "NestedCallbacks.hpp" #include "Storage.hpp" -#include - +#include +#include +#include +#include +#include #if ORB_TARGET_OS_DARWIN -#include +#include "PathMapper.hpp" +#include #endif +const uint8_t __ORBPathElementArgumentCount[25] = { + 2, 2, 4, 6, 0, 4, 4, 4, 6, 6, 8, 8, 5, 5, 5, 5, 0, 0, 2, 3, 0, 0, 2, 1, 1, +}; + namespace ORB { namespace Path { -atomic_uint Storage::_last_identifier; +namespace { +constexpr uint8_t elementFlags[] = { + 0, 0, 2, 4, 0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 32, 0, 32, 0, 32, 36, 32, +}; -Storage::Storage(uint32_t capacity) { - _unknown = nullptr; - if (capacity <= 63) { - precondition_failure("invalid capacity"); +enum class InfoKind { none, affine, stroke, boolean, clipStroke }; + +InfoKind info_kind(ORBPathElement element) { + switch (static_cast(element)) { + case 4: case 16: case 18: case 22: case 23: return InfoKind::none; + case 19: return InfoKind::stroke; + case 20: return InfoKind::boolean; + case 24: return InfoKind::clipStroke; + default: return InfoKind::affine; } - uint32_t cappedCapacity = fmin(capacity - 16, 4095); - _flags = StorageFlags().withCapacity(cappedCapacity); - _identifier = get_identifier(); } -Storage::Storage(uint32_t capacity, const Storage &storage): Storage(capacity) { - uint32_t originalCapity = flags().capacity(); - StorageFlags sourceFlags = storage.flags(); +size_t scalar_size(uint8_t encoding) { + return encoding & 0x40 ? sizeof(float) : encoding & 0x20 ? sizeof(int16_t) : sizeof(CGFloat); +} - _flags = _flags - .withExternal(false) - .withQuadBezierOrder(sourceFlags.isQuadBezierOrder()) - .withCubicBezierOrder(sourceFlags.isCubicBezierOrder()) - .withSingleElement(sourceFlags.isSingleElement()) - // bit 4 and 5 is from sourceFlags - // bit 6 and bit 7 is zero - .withCapacity(sourceFlags.capacity()); +void check_values(uint8_t &encoding, const CGFloat *values, size_t count) { + for (size_t i = 0; encoding && i < count; ++i) { + CGFloat magnitude = std::abs(values[i]); + if (!(magnitude <= 32767)) { + encoding = 0; + } else if ((encoding & 0x20) && std::abs(std::floor(magnitude + 0.5) - magnitude) > static_cast(1e-5f)) { + encoding &= ~0x20; + } + } +} - const Storage* sourceStorage = storage.actual_storage(); - uint64_t sourceSize = storage.actual_size(); +template +T read_value(const uint8_t *bytes, size_t &offset) { + T value; + std::memcpy(&value, bytes + offset, sizeof(T)); + offset += sizeof(T); + return value; +} - if (originalCapity < sourceSize) { - // reserve_slow(sourceSize); - sourceStorage = external_storage(); - if (flags().isExternal()) { - // sourceStorage = external_storage()->0; - } +void read_values(const uint8_t *bytes, size_t &offset, uint8_t encoding, CGFloat *values, size_t count) { + for (size_t i = 0; i < count; ++i) { + values[i] = encoding & 0x40 ? read_value(bytes, offset) + : encoding & 0x20 ? read_value(bytes, offset) + : read_value(bytes, offset); + } +} + +#if ORB_TARGET_OS_DARWIN +const ORBPathCallbacks storageCallbacks = { + .flags = {}, + .retain = nullptr, + .release = nullptr, + .apply = +[](const void *object, void *info, ORBPathApplyCallback callback) { + return static_cast(object)->apply_elements(info, callback); + }, + .isEqual = nullptr, + .isEmpty = nullptr, + .isSingleElement = +[](const void *object) { + return static_cast(object)->flags().isSingleElement(); + }, + .bezierOrder = nullptr, + .boundingRect = nullptr, + .cgPath = nullptr, + .next = nullptr, +}; +#endif + +} // namespace + +std::atomic_uint Storage::_last_identifier; + +size_t Storage::argument_count(ORBPathElement element) ORB_NOEXCEPT { + auto index = static_cast(element); + return index < sizeof(__ORBPathElementArgumentCount) ? __ORBPathElementArgumentCount[index] : 0; +} + +Storage::Storage(size_t capacity) : _bounds(0), _flags(), _identifier(get_identifier()) { + if (capacity < 64) { + precondition_failure("invalid capacity"); } - memcpy((void *)external_storage(), (const void *)sourceStorage, sourceSize); - // // Update our capacity info with the size - // if (!(flags() & 0x1)) { - // // Inline storage - update size bits - // _flags = (flags() & ~(0xFFF << 8)) | (sourceSize << 8); - // } else { - // // External storage - store size externally - // *reinterpret_cast(this + 0x18) = sourceSize; - // } + _flags = _flags.withCapacity(static_cast(std::min(capacity - 16, 4095))); +} + +Storage::Storage(size_t capacity, const Storage &storage) : Storage(capacity) { + _flags = StorageFlags((_flags.value & ~0x3eu) | (storage._flags.value & 0x3eu)); + push_bytes(storage.actual_storage(), storage.actual_size()); } Storage::~Storage() { - if (_unknown != nullptr) { - auto oldValue = _unknown; - _unknown = nullptr; - // TODO + invalidate(); + if (_flags.isExternal()) { + std::free(_external_storage); } - // TODO: MapCache - if (flags().isExternal()) { - #if ORB_TARGET_OS_DARWIN - if (cachedCGPath() != nullptr) { - auto oldCache = cachedCGPath(); - _cached_cgPath = nullptr; - CFRelease(oldCache); +} + +const uint8_t *Storage::actual_storage() const ORB_NOEXCEPT { + return _flags.isExternal() ? _external_storage : reinterpret_cast(this) + 16; +} + +uint8_t *Storage::bytes() ORB_NOEXCEPT { + return const_cast(actual_storage()); +} + +size_t Storage::actual_size() const ORB_NOEXCEPT { + return _flags.isExternal() ? _external_size : _flags.size(); +} + +size_t Storage::capacity() const ORB_NOEXCEPT { + return _flags.isExternal() ? _external_capacity : _flags.capacity(); +} + +void Storage::set_size(size_t size) ORB_NOEXCEPT { + if (_flags.isExternal()) { + _external_size = size; + } else { + _flags = _flags.withSize(static_cast(size)); + } +} + +void Storage::reserve_slow(size_t size) { + size_t currentCapacity = capacity(); + size_t doubled = currentCapacity <= std::numeric_limits::max() / 2 ? currentCapacity * 2 : size; + size_t newCapacity = std::max(doubled, size); + #if ORB_TARGET_OS_DARWIN + newCapacity = malloc_good_size(newCapacity); + #endif + if (_flags.isExternal()) { + auto *newBytes = static_cast(std::realloc(_external_storage, newCapacity)); + if (!newBytes) { + precondition_failure("failed to allocate path storage"); } + _external_storage = newBytes; + _external_capacity = newCapacity; + } else { + auto *newBytes = static_cast(std::malloc(newCapacity)); + if (!newBytes) { + precondition_failure("failed to allocate path storage"); + } + size_t size = actual_size(); + std::memcpy(newBytes, actual_storage(), size); + _flags = _flags.withExternal(true); + _external_storage = newBytes; + _external_size = size; + _external_capacity = newCapacity; + #if ORB_TARGET_OS_DARWIN + _cached_cgPath = nullptr; #endif - free((void *)external_storage()); } } +void Storage::push_bytes(const void *values, size_t count) { + size_t oldSize = actual_size(); + if (count > std::numeric_limits::max() - oldSize) { + precondition_failure("path storage size overflow"); + } + size_t size = oldSize + count; + if (size > capacity()) { + reserve_slow(size); + } + if (count) { + std::memcpy(bytes() + oldSize, values, count); + } + set_size(size); +} + +void Storage::push_values(uint8_t encoding, const CGFloat *values, size_t count) { + if (!(encoding & 0x60)) { + push_bytes(values, count * sizeof(CGFloat)); + return; + } + for (size_t i = 0; i < count; ++i) { + if (encoding & 0x40) { + float value = static_cast(values[i]); + push_bytes(&value, sizeof(value)); + } else { + int16_t value = static_cast(std::floor(values[i] + 0.5)); + push_bytes(&value, sizeof(value)); + } + } +} + +void Storage::invalidate() { +#if ORB_TARGET_OS_DARWIN + if (_flags.requiresMapping()) { + invalidate_mapped_storage(_identifier); + } +#endif + uintptr_t bounds = __atomic_exchange_n(&_bounds, 0, __ATOMIC_ACQ_REL); + if (bounds & 1) { + delete[] reinterpret_cast(bounds & ~uintptr_t(3)); + } + #if ORB_TARGET_OS_DARWIN + if (_flags.isExternal()) { + CGPathRef path = __atomic_exchange_n(&_cached_cgPath, nullptr, __ATOMIC_ACQ_REL); + if (path) { + CGPathRelease(path); + } + } + #endif +} + bool Storage::operator==(const Storage &other) const ORB_NOEXCEPT { - // TODO - return false; + size_t size = actual_size(); + return size == other.actual_size() + && ((_flags.value ^ other._flags.value) & 0x3e) == 0 + && std::memcmp(actual_storage(), other.actual_storage(), size) == 0; } void Storage::clear() { - // TODO + invalidate(); + set_size(0); + _flags = StorageFlags(_flags.value & ~0x3eu); } -bool Storage::append_element(ORBPathElement element, const CGFloat *points, const void *info) { - if (element >= ORBPathElementInvalid) { +bool Storage::append_element(ORBPathElement element, const CGFloat *points, const void *userInfo) { + auto type = static_cast(element); + if (type >= sizeof(__ORBPathElementArgumentCount)) { precondition_failure("invalid path element: %d", element); } - // TODO: Implement element appending - precondition_failure("TODO"); + if ((type == 8 || type == 9) && points[4] == 0 && points[5] == 0) { + type = 6; + } else if ((type == 10 || type == 11) && points[4] == 0 && points[5] == 0 && points[6] == 0 && points[7] == 0) { + type = 6; + } + element = static_cast(type); + InfoKind kind = userInfo ? info_kind(element) : InfoKind::none; + if (kind == InfoKind::affine) { + auto *transform = static_cast(userInfo); + if (transform[0] == 1 && transform[1] == 0 && transform[2] == 0 && transform[3] == 1 && transform[4] == 0 && transform[5] == 0) { + kind = InfoKind::none; + } + } + uint8_t encoding = 0x60; + check_values(encoding, points, __ORBPathElementArgumentCount[type]); + if (kind == InfoKind::affine) { + check_values(encoding, static_cast(userInfo), 6); + } else if (kind == InfoKind::stroke) { + auto &stroke = *static_cast(userInfo); + check_values(encoding, static_cast(stroke.values), std::min(stroke.count, 4096)); + } + encoding = encoding & 0x20 ? 0x20 : encoding & 0x40; + encoding |= static_cast(type) | (kind == InfoKind::none ? 0 : 0x80); + + invalidate(); + _flags = _flags.withSingleElement(isEmpty()); + _flags = StorageFlags(_flags.value | elementFlags[type]); + if (kind == InfoKind::stroke || kind == InfoKind::clipStroke) { + auto &stroke = *static_cast(userInfo); + if (stroke.join == 1 || (kind == InfoKind::stroke && stroke.cap == 1)) { + _flags = _flags.withCubicBezierOrder(true); + } + } + push_bytes(&encoding, 1); + push_values(encoding, points, __ORBPathElementArgumentCount[type]); + switch (kind) { + case InfoKind::none: + break; + case InfoKind::affine: + push_values(encoding, static_cast(userInfo), 6); + break; + case InfoKind::boolean: + push_bytes(userInfo, 2); + break; + case InfoKind::stroke: + case InfoKind::clipStroke: { + auto &stroke = *static_cast(userInfo); + uint8_t cap = static_cast(stroke.cap); + uint8_t join = static_cast(stroke.join); + uint16_t count = static_cast(std::min(stroke.count, kind == InfoKind::stroke ? 4096 : 2048)); + push_bytes(&cap, 1); + push_bytes(&join, 1); + push_bytes(&count, 2); + if (kind == InfoKind::stroke) { + push_values(encoding, static_cast(stroke.values), count); + } else { + auto *records = static_cast(stroke.values); + for (size_t i = 0; i < count; ++i) { + uint8_t type = static_cast(records[i].type); + push_bytes(&type, 1); + push_bytes(records[i].values, sizeof(records[i].values)); + } + } + break; + } + } + if (type == 4 || type == 16) { + update_single_element(); + } + return true; +} + +void Storage::append_storage(const Storage &storage) { + if (storage.isEmpty()) { + return; + } + StorageFlags sourceFlags = storage._flags; + std::vector copy; + const uint8_t *source = storage.actual_storage(); + size_t count = storage.actual_size(); + if (this == &storage) { + copy.assign(source, source + count); + source = copy.data(); + } + invalidate(); + _flags = _flags.withSingleElement(isEmpty() && sourceFlags.isSingleElement()); + _flags = StorageFlags(_flags.value | (sourceFlags.value & 0x26)); + push_bytes(source, count); +} + +void Storage::update_single_element() { + if (_flags.value & 0x10) { + return; + } + NestedCallbacks::SingleInfo info{}; + NestedCallbacks::single_element_fast(*this, &info); + _flags = StorageFlags(_flags.value & ~0x18u); + if (info.currentDepth == 0 && info.count != 0) { + _flags = StorageFlags(_flags.value | (info.count == 1 ? 8 : 16)); + } +} + +bool Storage::Iterator::next(const Storage &storage, Element &element) { + if (offset >= storage.actual_size()) { + return false; + } + const uint8_t *bytes = storage.actual_storage(); + uint8_t encoding = read_value(bytes, offset); + element.type = static_cast(encoding & 0x1f); + read_values(bytes, offset, encoding, element.points, argument_count(element.type)); + InfoKind kind = encoding & 0x80 ? info_kind(element.type) : InfoKind::none; + element.hasInfo = kind != InfoKind::none; + switch (kind) { + case InfoKind::none: + break; + case InfoKind::affine: + read_values(bytes, offset, encoding, element.metadata.transform, 6); + break; + case InfoKind::boolean: + std::memcpy(element.metadata.boolean, bytes + offset, 2); + offset += 2; + break; + case InfoKind::stroke: + case InfoKind::clipStroke: { + auto &stroke = element.metadata.stroke; + stroke.cap = read_value(bytes, offset); + stroke.join = read_value(bytes, offset); + stroke.count = read_value(bytes, offset); + if (kind == InfoKind::stroke) { + element.values.resize(stroke.count); + read_values(bytes, offset, encoding, element.values.data(), stroke.count); + stroke.values = element.values.data(); + } else { + element.records.resize(stroke.count); + for (auto &record : element.records) { + record.type = read_value(bytes, offset); + std::memcpy(record.values, bytes + offset, sizeof(record.values)); + offset += sizeof(record.values); + } + stroke.values = element.records.data(); + } + break; + } + } + return true; +} + +bool Storage::Iterator::next_type(const Storage &storage, ORBPathElement &element) { + if (offset >= storage.actual_size()) { + return false; + } + const uint8_t *bytes = storage.actual_storage(); + uint8_t encoding = read_value(bytes, offset); + element = static_cast(encoding & 0x1f); + size_t width = scalar_size(encoding); + offset += argument_count(element) * width; + if (encoding & 0x80) { + switch (info_kind(element)) { + case InfoKind::none: break; + case InfoKind::affine: offset += 6 * width; break; + case InfoKind::boolean: offset += 2; break; + case InfoKind::stroke: + case InfoKind::clipStroke: { + offset += 2; + size_t count = read_value(bytes, offset); + offset += count * (static_cast(element) == 19 ? width : 13); + break; + } + } + } + return true; } bool Storage::apply_elements(void *info, ORBPathApplyCallback callback) const ORB_NOEXCEPT { - // TODO: Add fast-path checks for special callbacks: - // - append_element_callback → append_storage(info, this) - // - NestedCallbacks::apply_elements_callback → apply_elements_fast - // - NestedCallbacks::single_element_callback → single_element_fast - // - Mapper::apply_callback with flags check → MapCache::apply + if (!callback) { + return true; + } + if (callback == append_element_callback) { + static_cast(info)->append_storage(*this); + return true; + } + if (callback == NestedCallbacks::apply_elements_callback) { + return NestedCallbacks::apply_elements_fast(*this, info); + } + if (callback == NestedCallbacks::single_element_callback) { + return NestedCallbacks::single_element_fast(*this, info); + } +#if ORB_TARGET_OS_DARWIN + if (_flags.requiresMapping() && callback == apply_mapping_element) { + return apply_mapped_storage(*this, info, 0); + } +#endif return apply_elements_(info, callback); } bool Storage::apply_elements_(void *info, ORBPathApplyCallback callback) const ORB_NOEXCEPT { - // TODO: Implement actual element iteration over storage + Iterator iterator; + Element element; + while (iterator.next(*this, element)) { + if (!callback(info, element.type, element.points, element.info())) { + return false; + } + } + return true; +} + +bool Storage::apply_elements_at_depth(void *info, ORBPathApplyCallback callback, size_t depth) const ORB_NOEXCEPT { +#if ORB_TARGET_OS_DARWIN + if (_flags.requiresMapping() && callback == apply_mapping_element) { + return apply_mapped_storage(*this, info, depth); + } +#endif + if (depth == 0) { + return apply_elements_(info, callback); + } + Iterator iterator; + if (!NestedCallbacks::first_element(*this, iterator, depth)) { + return false; + } + Element element; + size_t currentDepth = depth; + while (iterator.next(*this, element)) { + if (NestedCallbacks::is_nesting_element(element.type)) { + ++currentDepth; + } else if (static_cast(element.type) == 16) { + if (currentDepth == depth) { + return true; + } + --currentDepth; + } + if (!callback(info, element.type, element.points, element.info())) { + return false; + } + } return true; } #if ORB_TARGET_OS_DARWIN +CGRect Storage::bounding_rect() const ORB_NOEXCEPT { + uintptr_t cache = __atomic_load_n(&_bounds, __ATOMIC_ACQUIRE); + if (cache) { + uintptr_t address = (cache & 2 ? reinterpret_cast(this) : 0) + cache; + auto *values = reinterpret_cast(address & ~uintptr_t(3)); + return CGRectMake(values[0], values[1], values[2], values[3]); + } + if (_flags.isSingleElement()) { + uint8_t encoding = *actual_storage(); + unsigned type = encoding & 0x1f; + if (!(encoding & 0x80) && type >= 5 && type <= 11) { + size_t offset = 1; + CGFloat values[4]; + read_values(actual_storage(), offset, encoding, values, 4); + return CGRectMake(values[0], values[1], values[2], values[3]); + } + } + ORBPath path{reinterpret_cast(const_cast(this)), &storageCallbacks}; + CGRect result = ORBPathGetBoundingRect(path); + float *values; + if (_flags.isExternal()) { + values = _external_bounds; + cache = 0x2a; + } else { + uintptr_t start = reinterpret_cast(actual_storage()); + uintptr_t end = start + capacity(); + uintptr_t aligned = (start + actual_size() + 3) & ~uintptr_t(3); + if (end >= aligned && end - aligned >= sizeof(float) * 4) { + values = reinterpret_cast(aligned); + cache = (aligned - reinterpret_cast(this)) | 2; + } else { + values = new float[4]; + cache = reinterpret_cast(values) | 1; + } + } + values[0] = static_cast(result.origin.x); + values[1] = static_cast(result.origin.y); + values[2] = static_cast(result.size.width); + values[3] = static_cast(result.size.height); + result = CGRectMake(values[0], values[1], values[2], values[3]); + uintptr_t expected = 0; + if (!__atomic_compare_exchange_n(&_bounds, &expected, cache, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE) && (cache & 1)) { + delete[] values; + } + return result; +} + CGPathRef Storage::cgpath() const ORB_NOEXCEPT { - if (flags().isInline()) { + if (_flags.isInline()) { return nullptr; } - CGPathRef cached = __atomic_load_n(&_cached_cgPath, __ATOMIC_SEQ_CST); - if (cached != nullptr) { + CGPathRef cached = __atomic_load_n(&_cached_cgPath, __ATOMIC_ACQUIRE); + if (cached) { return cached; } - static const ORBPathCallbacks callbacks = { - {}, - nullptr, - nullptr, - .apply = +[](const void *object, void *info, ORBPathApplyCallback callback) -> bool { - auto storage = reinterpret_cast(object); - return storage->apply_elements(info, callback); - }, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - }; - ORBPath path = { - const_cast(reinterpret_cast(this)), - &callbacks - }; - CGPathRef new_path = ORBPathCopyCGPath(path); + ORBPath path{reinterpret_cast(const_cast(this)), &storageCallbacks}; + CGPathRef newPath = ORBPathCopyCGPath(path); CGPathRef expected = nullptr; - if (__atomic_compare_exchange_n(&_cached_cgPath, &expected, new_path, - false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) { - return new_path; - } else { - CGPathRelease(new_path); - return expected; + if (__atomic_compare_exchange_n(&_cached_cgPath, &expected, newPath, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) { + return newPath; } + CGPathRelease(newPath); + return expected; } #endif diff --git a/Sources/OpenRenderBoxCxx/Path/Storage.hpp b/Sources/OpenRenderBoxCxx/Path/Storage.hpp index 265d4c3..c9a9226 100644 --- a/Sources/OpenRenderBoxCxx/Path/Storage.hpp +++ b/Sources/OpenRenderBoxCxx/Path/Storage.hpp @@ -10,7 +10,10 @@ #include #include "../Util/assert.hpp" -#include +#include +#include +#include +#include #if ORB_TARGET_OS_DARWIN #include #endif @@ -29,6 +32,7 @@ struct StorageFlags { /// 3 -> isSingleElement /// [8, 20) -> size /// [20, 32) -> capacity + friend class Storage; uint32_t value; ORB_INLINE ORB_CONSTEXPR @@ -85,6 +89,11 @@ struct StorageFlags { return (value & 8u) != 0; } + ORB_INLINE ORB_CONSTEXPR + bool requiresMapping() const ORB_NOEXCEPT { + return (value & 32u) != 0; + } + ORB_INLINE ORB_CONSTEXPR uint32_t size() const ORB_NOEXCEPT { return (value >> 8u) & 0xFFFu; @@ -122,7 +131,7 @@ struct StorageFlags { ORB_INLINE ORB_CONSTEXPR StorageFlags withCapacity(uint32_t capacity) const ORB_NOEXCEPT { - return StorageFlags((value & 0xFFFFFFu) | ((capacity & 0xFFFu) << 0x14)); + return StorageFlags((value & 0x000FFFFFu) | ((capacity & 0xFFFu) << 0x14)); } ORB_INLINE ORB_CONSTEXPR @@ -133,113 +142,115 @@ struct StorageFlags { class Storage { public: + struct StrokeInfo { + int32_t cap; + int32_t join; + size_t count; + const void * _Nullable values; + }; + + struct StrokeRecord { + int32_t type; + uint8_t values[12]; + }; + + struct Element { + CGFloat points[8] = {}; + ORBPathElement type = ORBPathElementMoveToPoint; + bool hasInfo = false; + union { + CGFloat transform[6]; + StrokeInfo stroke; + uint8_t boolean[2]; + } metadata = {}; + std::vector values; + std::vector records; + + const void * _Nullable info() const ORB_NOEXCEPT { + return hasInfo ? &metadata : nullptr; + } + }; + + struct Iterator { + size_t offset = 0; + bool next(const Storage &storage, Element &element); + bool next_type(const Storage &storage, ORBPathElement &element); + }; + + static size_t argument_count(ORBPathElement element) ORB_NOEXCEPT; + ORB_INLINE ORB_CONSTEXPR - const static atomic_uint& last_identifier() ORB_NOEXCEPT { + static const std::atomic_uint &last_identifier() ORB_NOEXCEPT { return _last_identifier; } ORB_INLINE - const static uint32_t get_identifier() ORB_NOEXCEPT { - return atomic_fetch_add_explicit(&_last_identifier, 1, memory_order_relaxed); + static uint32_t get_identifier() ORB_NOEXCEPT { + return _last_identifier.fetch_add(1, std::memory_order_relaxed) + 1; } -private: - static atomic_uint _last_identifier; - -public: - Storage(uint32_t capacity); - Storage(uint32_t capacity, const ORB::Path::Storage &storage); + Storage(size_t capacity); + Storage(size_t capacity, const Storage &storage); + Storage(const Storage &) = delete; + Storage &operator=(const Storage &) = delete; ~Storage(); - bool operator == (const Storage &other) const ORB_NOEXCEPT; - + bool operator==(const Storage &other) const ORB_NOEXCEPT; void clear(); - bool append_element(ORBPathElement element, const CGFloat *points, const void * _Nullable userInfo); -// push_values(unsigned char, double const*, unsigned long) - // update_single_element() - - /// Apply callback to each path element (with fast-path checks) - bool apply_elements(void *info, ORBPathApplyCallback callback) const ORB_NOEXCEPT; - -private: - /// Core element iteration (no fast-path checks) - bool apply_elements_(void *info, ORBPathApplyCallback callback) const ORB_NOEXCEPT; - -public: + void append_storage(const Storage &storage); + bool apply_elements(void *info, ORBPathApplyCallback _Nullable callback) const ORB_NOEXCEPT; + bool apply_elements_at_depth(void *info, ORBPathApplyCallback callback, size_t depth) const ORB_NOEXCEPT; #if ORB_TARGET_OS_DARWIN - /// Get cached CGPath, lazily creating if needed (thread-safe with casal) + CGRect bounding_rect() const ORB_NOEXCEPT; CGPathRef _Nullable cgpath() const ORB_NOEXCEPT; #endif -public: - ORB_INLINE ORB_CONSTEXPR - void * unknown() const ORB_NOEXCEPT { - return _unknown; - } - - ORB_INLINE ORB_CONSTEXPR - StorageFlags flags() const ORB_NOEXCEPT { - return _flags; - } ORB_INLINE ORB_CONSTEXPR - uint32_t identifier() const ORB_NOEXCEPT { - return _identifier; - } - - ORB_INLINE ORB_CONSTEXPR - const Storage* external_storage() const ORB_NOEXCEPT { - return _external_storage; - } - - ORB_INLINE ORB_CONSTEXPR - uint64_t external_size() const ORB_NOEXCEPT { - return _external_size; - } - - ORB_INLINE ORB_CONSTEXPR - const Storage* actual_storage() const ORB_NOEXCEPT { - if (flags().isInline()) { - return this; - } else { - return external_storage(); - } - } + StorageFlags flags() const ORB_NOEXCEPT { return _flags; } ORB_INLINE ORB_CONSTEXPR - uint64_t actual_size() const ORB_NOEXCEPT { - if (flags().isInline()) { - return flags().size(); - } else { - return external_size(); - } - } + uint32_t identifier() const ORB_NOEXCEPT { return _identifier; } - ORB_INLINE ORB_CONSTEXPR - bool isEmpty() const ORB_NOEXCEPT { - return actual_size() == 0; - } + const uint8_t *actual_storage() const ORB_NOEXCEPT; + size_t actual_size() const ORB_NOEXCEPT; - #if ORB_TARGET_OS_DARWIN ORB_INLINE - CGPathRef cachedCGPath() const ORB_NOEXCEPT { - return _cached_cgPath; - } - #endif + bool isEmpty() const ORB_NOEXCEPT { return actual_size() == 0; } + private: - void * _unknown; // 0x00 + static std::atomic_uint _last_identifier; + uint8_t *bytes() ORB_NOEXCEPT; + size_t capacity() const ORB_NOEXCEPT; + void set_size(size_t size) ORB_NOEXCEPT; + void reserve_slow(size_t size); + void push_bytes(const void *values, size_t count); + void push_values(uint8_t encoding, const CGFloat *values, size_t count); + void invalidate(); + void update_single_element(); + bool apply_elements_(void *info, ORBPathApplyCallback callback) const ORB_NOEXCEPT; + + mutable uintptr_t _bounds; // 0x00: tagged bounds cache StorageFlags _flags; // 0x08 - uint32_t _identifier; // 0x0C - Storage* _external_storage; // 0x10 - uint64_t _external_size; // 0x18 - void * _reserved1; // 0x20 - void * _reserved2; // 0x28 - void * _reserved3; // 0x30 + uint32_t _identifier; // 0x0c + // Inline bytes start at 0x10 and can extend to the caller's capacity. + // These fields are live only when flags bit 0 selects external storage. + uint8_t *_external_storage; // 0x10 + size_t _external_size; // 0x18 + size_t _external_capacity; // 0x20 + mutable float _external_bounds[4]; // 0x28 #if ORB_TARGET_OS_DARWIN - mutable CGPathRef _cached_cgPath; // 0x38 - lazily cached CGPath + mutable CGPathRef _Nullable _cached_cgPath; // 0x38 + #else + void *_reserved; // 0x38 #endif }; + +static_assert(sizeof(Storage) == 64, "Storage header and external overlay must fit 64 bytes"); + +bool append_element_callback(void *info, ORBPathElement element, const CGFloat *points, const void * _Nullable userInfo); + } /* Path */ } /* ORB */ diff --git a/Sources/OpenRenderBoxCxx/include/module.modulemap b/Sources/OpenRenderBoxCxx/include/module.modulemap index c1389b4..8582676 100644 --- a/Sources/OpenRenderBoxCxx/include/module.modulemap +++ b/Sources/OpenRenderBoxCxx/include/module.modulemap @@ -22,10 +22,10 @@ module OpenRenderBoxCxx.C { header "OpenRenderBox/ORBSymbolRenderingMode.h" header "OpenRenderBox/ORBUUID.h" header "OpenRenderBox/ORBVersion.h" - export * } -module OpenRenderBoxCxx.Color { +// These modules import system CoreGraphics and must be selected explicitly. +explicit module OpenRenderBoxCxx.Color { requires cplusplus header "../Color/ColorSpace.hpp" export * @@ -43,7 +43,14 @@ module OpenRenderBoxCxx.Animation { module OpenRenderBoxCxx.Path { requires cplusplus + header "../Path/ClipStrokeKeyframes.hpp" header "../Path/NestedCallbacks.hpp" + header "../Path/PathClipStroke.hpp" + header "../Path/PathInterpolation.hpp" + header "../Path/PathGeometry.hpp" + header "../Path/PathMapper.hpp" + header "../Path/PathStorageObject.hpp" + header "../Path/RoundedRect.hpp" header "../Path/Storage.hpp" export * } @@ -56,7 +63,7 @@ module OpenRenderBoxCxx.Util { export * } -module OpenRenderBoxCxx.ObjC { +explicit module OpenRenderBoxCxx.ObjC { requires objc header "OpenRenderBox/ORBAnimation.h" header "OpenRenderBox/ORBSymbolAnimator.h" diff --git a/Sources/OpenRenderBoxShims/ORBShims.swift b/Sources/OpenRenderBoxShims/ORBShims.swift index 230c10d..a9c9dfb 100644 --- a/Sources/OpenRenderBoxShims/ORBShims.swift +++ b/Sources/OpenRenderBoxShims/ORBShims.swift @@ -37,8 +37,23 @@ public typealias ORBDisplayListTransform = RBDisplayListTransform public typealias ORBDrawingState = RBDrawingState public typealias ORBLayer = RBLayer public typealias ORBLayerDelegate = RBLayerDelegate +public let __ORBPathElementArgumentCount = __RBPathElementArgumentCount public typealias ORBPath = RBPath public typealias ORBPathApplyCallback = RBPathApplyCallback +public typealias ORBPathApplierCallback = RBPathApplierCallback +public typealias ORBPathLineApplyCallback = RBPathLineApplyCallback +public typealias ORBPathLineElement = RBPathLineElement +public typealias ORBPathApplyShapeCallback = RBPathApplyShapeCallback +public typealias ORBPathFixedRoundedRectShape = RBPathFixedRoundedRectShape +public typealias ORBPathLineShape = RBPathLineShape +public typealias ORBPathNestedShape = RBPathNestedShape +public typealias ORBPathRectShape = RBPathRectShape +public typealias ORBPathShape = RBPathShape +public typealias ORBPathShapeType = RBPathShapeType +public typealias ORBPathStrokedShape = RBPathStrokedShape +public typealias ORBPathStrokeStyle = RBPathStrokeStyle +public typealias ORBPathTrimmedShape = RBPathTrimmedShape +public typealias ORBPathUnevenRoundedRectShape = RBPathUnevenRoundedRectShape public typealias ORBUUID = RBUUID public typealias ORBTransition = RBTransition public typealias ORBTransitionEffect = RBTransitionEffect diff --git a/Sources/OpenRenderBoxShimsCxx/include/RenderBox/OpenRenderBoxShims/OpenRenderBoxShims.h b/Sources/OpenRenderBoxShimsCxx/include/RenderBox/OpenRenderBoxShims/OpenRenderBoxShims.h index 120aabe..8152b08 100644 --- a/Sources/OpenRenderBoxShimsCxx/include/RenderBox/OpenRenderBoxShims/OpenRenderBoxShims.h +++ b/Sources/OpenRenderBoxShimsCxx/include/RenderBox/OpenRenderBoxShims/OpenRenderBoxShims.h @@ -14,10 +14,24 @@ typedef RBPath ORBPath; typedef RBPathElement ORBPathElement; typedef RBPathRoundedCornerStyle ORBPathRoundedCornerStyle; typedef RBPathApplyCallback ORBPathApplyCallback; +typedef RBPathApplierCallback ORBPathApplierCallback; +typedef RBPathLineApplyCallback ORBPathLineApplyCallback; +typedef RBPathLineElement ORBPathLineElement; typedef RBPathCallbacks ORBPathCallbacks; typedef RBPathCallbacksFlags ORBPathCallbacksFlags; typedef RBPathCallbacksExtended ORBPathCallbacksExtended; typedef RBPathStorageRef ORBPathStorageRef; +typedef RBPathShapeType ORBPathShapeType; +typedef RBPathRectShape ORBPathRectShape; +typedef RBPathFixedRoundedRectShape ORBPathFixedRoundedRectShape; +typedef RBPathUnevenRoundedRectShape ORBPathUnevenRoundedRectShape; +typedef RBPathLineShape ORBPathLineShape; +typedef RBPathStrokeStyle ORBPathStrokeStyle; +typedef RBPathStrokedShape ORBPathStrokedShape; +typedef RBPathNestedShape ORBPathNestedShape; +typedef RBPathTrimmedShape ORBPathTrimmedShape; +typedef RBPathShape ORBPathShape; +typedef RBPathApplyShapeCallback ORBPathApplyShapeCallback; #define ORBBlendModeNormal RBBlendModeNormal #define ORBBlendModeMultiply RBBlendModeMultiply @@ -70,13 +84,39 @@ typedef RBPathStorageRef ORBPathStorageRef; #define ORBPathElementCloseSubpath RBPathElementCloseSubpath #define ORBPathElementRect RBPathElementRect #define ORBPathElementRoundedRect RBPathElementRoundedRect +#define ORBPathElementEllipse RBPathElementEllipse #define ORBPathElementFixedRoundedRectCircular RBPathElementFixedRoundedRectCircular #define ORBPathElementFixedRoundedRectContinuous RBPathElementFixedRoundedRectContinuous +#define ORBPathElementUnevenRoundedRectCircular RBPathElementUnevenRoundedRectCircular +#define ORBPathElementUnevenRoundedRectContinuous RBPathElementUnevenRoundedRectContinuous +#define ORBPathElementRelativeArc RBPathElementRelativeArc +#define ORBPathElementArc RBPathElementArc +#define ORBPathElementArcCounterclockwise RBPathElementArcCounterclockwise +#define ORBPathElementArcTo RBPathElementArcTo +#define ORBPathElementEnd RBPathElementEnd +#define ORBPathElementTransformed RBPathElementTransformed +#define ORBPathElementTrimmed RBPathElementTrimmed +#define ORBPathElementStroked RBPathElementStroked +#define ORBPathElementBoolean RBPathElementBoolean +#define ORBPathElementBooleanSeparator RBPathElementBooleanSeparator +#define ORBPathElementDilateGlyph RBPathElementDilateGlyph +#define ORBPathElementOffset RBPathElementOffset +#define ORBPathElementClipStroke RBPathElementClipStroke #define ORBPathElementInvalid RBPathElementInvalid #define ORBPathRoundedCornerStyleCircular RBPathRoundedCornerStyleCircular #define ORBPathRoundedCornerStyleContinuous RBPathRoundedCornerStyleContinuous +#define ORBPathShapeTypeRect RBPathShapeTypeRect +#define ORBPathShapeTypeEllipse RBPathShapeTypeEllipse +#define ORBPathShapeTypeLine RBPathShapeTypeLine +#define ORBPathShapeTypeFixedRoundedRect RBPathShapeTypeFixedRoundedRect +#define ORBPathShapeTypeUnevenRoundedRect RBPathShapeTypeUnevenRoundedRect +#define ORBPathShapeTypeNested RBPathShapeTypeNested +#define ORBPathShapeTypeStroked RBPathShapeTypeStroked +#define ORBPathShapeTypeTrimmed RBPathShapeTypeTrimmed +#define ORBPathShapeTypeOffset RBPathShapeTypeOffset + #define ORBPathEmpty RBPathEmpty #define ORBPathNull RBPathNull #define ORBPathRetain RBPathRetain @@ -86,6 +126,13 @@ typedef RBPathStorageRef ORBPathStorageRef; #define ORBPathMakeEllipse RBPathMakeEllipse #define ORBPathMakeRoundedRect RBPathMakeRoundedRect #define ORBPathMakeUnevenRoundedRect RBPathMakeUnevenRoundedRect +#define ORBPathCopyTransformedPath RBPathCopyTransformedPath +#define ORBPathCopyStrokedPath RBPathCopyStrokedPath +#define ORBPathCopyTrimmedPath RBPathCopyTrimmedPath +#define ORBPathIsSingleElement RBPathIsSingleElement +#define ORBPathGetBezierOrder RBPathGetBezierOrder +#define ORBPathApplyShape RBPathApplyShape +#define ORBPathGetBoundingRect RBPathGetBoundingRect #define ORBPathIsEmpty RBPathIsEmpty #define ORBPathApplyElements RBPathApplyElements #define ORBPathEqualToPath RBPathEqualToPath @@ -106,3 +153,17 @@ typedef RBPathStorageRef ORBPathStorageRef; #define ORBPathStorageGetBezierOrder RBPathStorageGetBezierOrder #define ORBPathStorageGetBoundingRect RBPathStorageGetBoundingRect #define ORBPathStorageGetCGPath RBPathStorageGetCGPath + +#define ORBPathApply RBPathApply +#define ORBPathApplyLines RBPathApplyLines +#define ORBPathGetLength RBPathGetLength +#define ORBPathCanInterpolate RBPathCanInterpolate +#define ORBPathCopyInterpolatedPath RBPathCopyInterpolatedPath +#define ORBPathCopyData RBPathCopyData +#define ORBPathMakeWithData RBPathMakeWithData +#define ORBPathCopyDebugDescription RBPathCopyDebugDescription +#define ORBPathLineElementMoveToPoint RBPathLineElementMoveToPoint +#define ORBPathLineElementAddLineToPoint RBPathLineElementAddLineToPoint +#define ORBPathLineElementCloseSubpath RBPathLineElementCloseSubpath + +#define __ORBPathElementArgumentCount __RBPathElementArgumentCount diff --git a/Tests/OpenRenderBoxCompatibilityTests/ORBShims.swift b/Tests/OpenRenderBoxCompatibilityTests/ORBShims.swift index 4080c01..8926e20 100644 --- a/Tests/OpenRenderBoxCompatibilityTests/ORBShims.swift +++ b/Tests/OpenRenderBoxCompatibilityTests/ORBShims.swift @@ -13,8 +13,23 @@ public typealias ORBDisplayListContents = RBDisplayListContents public typealias ORBDisplayListInterpolator = RBDisplayListInterpolator public typealias ORBLayer = RBLayer public typealias ORBLayerDelegate = RBLayerDelegate +public let __ORBPathElementArgumentCount = __RBPathElementArgumentCount public typealias ORBPath = RBPath public typealias ORBPathApplyCallback = RBPathApplyCallback +public typealias ORBPathApplierCallback = RBPathApplierCallback +public typealias ORBPathLineApplyCallback = RBPathLineApplyCallback +public typealias ORBPathLineElement = RBPathLineElement +public typealias ORBPathApplyShapeCallback = RBPathApplyShapeCallback +public typealias ORBPathFixedRoundedRectShape = RBPathFixedRoundedRectShape +public typealias ORBPathLineShape = RBPathLineShape +public typealias ORBPathNestedShape = RBPathNestedShape +public typealias ORBPathRectShape = RBPathRectShape +public typealias ORBPathShape = RBPathShape +public typealias ORBPathShapeType = RBPathShapeType +public typealias ORBPathStrokedShape = RBPathStrokedShape +public typealias ORBPathStrokeStyle = RBPathStrokeStyle +public typealias ORBPathTrimmedShape = RBPathTrimmedShape +public typealias ORBPathUnevenRoundedRectShape = RBPathUnevenRoundedRectShape public typealias ORBUUID = RBUUID public typealias ORBTransition = RBTransition public typealias ORBTransitionEffect = RBTransitionEffect @@ -32,6 +47,9 @@ public let compatibilityTestEnabled = true #else @_exported import OpenRenderBox @_exported import OpenRenderBoxCxx +#if canImport(ObjectiveC) +@_exported import OpenRenderBoxCxx.ObjC +#endif public let compatibilityTestEnabled = false #endif diff --git a/Tests/OpenRenderBoxCompatibilityTests/PathArcMappingTests.swift b/Tests/OpenRenderBoxCompatibilityTests/PathArcMappingTests.swift new file mode 100644 index 0000000..eed02d4 --- /dev/null +++ b/Tests/OpenRenderBoxCompatibilityTests/PathArcMappingTests.swift @@ -0,0 +1,60 @@ +// +// PathArcMappingTests.swift +// OpenRenderBoxCompatibilityTests + +#if canImport(CoreGraphics) && (OPENRENDERBOX_CF_CGTYPES || OPENRENDERBOX_COMPATIBILITY_TEST) +import CoreGraphics +import Foundation +import Testing + +struct PathArcMappingTests { + @Test(arguments: [ORBPath.Element.arc, .arcCounterclockwise]) + func quarterTurnUsesStableSegments(element: ORBPath.Element) { + withPathStorage { storage in + storage.append(element: element, points: [0, 0, 10, 0, .pi / 2], userInfo: nil) + withStoragePath(storage) { path in + let elements = cgPathElements(path.cgPath) + #expect(elements.count == (element == .arc ? 4 : 2)) + #expect(elements.first?.element == 0) + let end = path.cgPath.currentPoint + #expect(abs(end.x) < 0.00001) + #expect(abs(end.y - 10) < 0.00001) + } + } + } + + @Test(arguments: [ORBPath.Element.arc, .arcCounterclockwise]) + func fullTurnClosesTheCircle(element: ORBPath.Element) { + withPathStorage { storage in + storage.append(element: element, points: [0, 0, 10, 0, 2 * .pi], userInfo: nil) + withStoragePath(storage) { path in + let elements = cgPathElements(path.cgPath) + #expect(elements.count == 6) + #expect(elements.last?.element == 4) + #expect(path.boundingRect == CGRect(x: -10, y: -10, width: 20, height: 20)) + } + } + } + + @Test + func relativeArcPreservesTwoTurns() { + withPathStorage { storage in + storage.append(element: .relativeArc, points: [0, 0, 10, 0, 4 * .pi], userInfo: nil) + withStoragePath(storage) { path in + #expect(cgPathElements(path.cgPath).count == 9) + #expect(path.boundingRect == CGRect(x: -10, y: -10, width: 20, height: 20)) + } + } + } + + @Test + func relativeArcOmitsSmallResiduals() { + withPathStorage { storage in + storage.append(element: .relativeArc, points: [0, 0, 10, 0, 0.000001], userInfo: nil) + withStoragePath(storage) { path in + #expect(cgPathElements(path.cgPath) == [PathElementRecord(element: 0, points: [10, 0])]) + } + } + } +} +#endif diff --git a/Tests/OpenRenderBoxCompatibilityTests/PathCallbacksTests.swift b/Tests/OpenRenderBoxCompatibilityTests/PathCallbacksTests.swift index 31bc75f..4a4ff19 100644 --- a/Tests/OpenRenderBoxCompatibilityTests/PathCallbacksTests.swift +++ b/Tests/OpenRenderBoxCompatibilityTests/PathCallbacksTests.swift @@ -4,11 +4,21 @@ import Testing -#if canImport(CoreGraphics) +#if canImport(CoreGraphics) && (OPENRENDERBOX_CF_CGTYPES || OPENRENDERBOX_COMPATIBILITY_TEST) import CoreGraphics -@Suite(.enabled(if: compatibilityTestEnabled)) +@Suite struct PathCallbacksTests { + @Test + func cgPathBezierOrder() { + let cgPath = CGMutablePath() + cgPath.move(to: .zero) + cgPath.addCurve(to: CGPoint(x: 10, y: 0), control1: CGPoint(x: 0, y: 10), control2: CGPoint(x: 10, y: 10)) + let path = ORBPath(cgPath: cgPath) + defer { path.release() } + #expect(path.bezierOrder == 3) + } + @Test func cgPathCallbacks() { let callbacks = ORBPath.Callbacks.cgPath @@ -19,7 +29,6 @@ struct PathCallbacksTests { #expect(callbacks.isEqual != nil) #expect(callbacks.isEmpty != nil) #expect(callbacks.isSingleElement != nil) - #expect(callbacks.bezierOrder != nil) #expect(callbacks.boundingRect != nil) #expect(callbacks.cgPath != nil) #expect(callbacks.next == nil) diff --git a/Tests/OpenRenderBoxCompatibilityTests/PathOperationsTests.swift b/Tests/OpenRenderBoxCompatibilityTests/PathOperationsTests.swift new file mode 100644 index 0000000..43b2b85 --- /dev/null +++ b/Tests/OpenRenderBoxCompatibilityTests/PathOperationsTests.swift @@ -0,0 +1,233 @@ +// +// PathOperationsTests.swift +// OpenRenderBoxCompatibilityTests + +import Testing + +#if canImport(CoreGraphics) && (OPENRENDERBOX_CF_CGTYPES || OPENRENDERBOX_COMPATIBILITY_TEST) +import CoreGraphics +import Foundation + +@Suite +struct PathOperationsTests { + private func curve(quadratic: Bool) -> ORBPath { + let path = CGMutablePath() + path.move(to: CGPoint(x: 0.1, y: 0.2)) + if quadratic { + path.addQuadCurve(to: CGPoint(x: 10, y: 0), control: CGPoint(x: 5, y: 10)) + } else { + path.addCurve(to: CGPoint(x: 10, y: 0), control1: CGPoint(x: 0, y: 10), control2: CGPoint(x: 10, y: 10)) + } + path.closeSubpath() + return ORBPath(cgPath: path) + } + + @Test + func applyTransformsElementsAndStopsOnCancellation() { + let path = curve(quadratic: false) + defer { path.release() } + var transform = CGAffineTransform(a: 2, b: 0, c: 0, d: 3, tx: 7, ty: 11) + var elements: [PathElementRecord] = [] + let completed = withUnsafeMutablePointer(to: &elements) { + path.applyCGPathElements(transform: &transform, info: $0) { info, element in + let element = element.pointee + let count = element.type == .addCurveToPoint ? 3 : element.type == .closeSubpath ? 0 : 1 + let points = UnsafeBufferPointer(start: element.points, count: count).flatMap { [$0.x, $0.y] } + info.assumingMemoryBound(to: [PathElementRecord].self).pointee.append( + PathElementRecord(element: element.type.rawValue, points: points) + ) + return true + } + } + #expect(completed) + #expect(elements == [ + PathElementRecord(element: 0, points: [7.2, 11.6]), + PathElementRecord(element: 3, points: [7, 41, 27, 41, 27, 11]), + PathElementRecord(element: 4, points: []), + ]) + var count = 0 + #expect(!path.applyCGPathElements(transform: nil, info: &count) { info, _ in + let count = info.assumingMemoryBound(to: Int.self) + count.pointee += 1 + return count.pointee < 2 + }) + #expect(count == 2) + count = 0 + #expect(!path.applyLines(tolerance: 0.5, info: &count) { info, _, _ in + let count = info.assumingMemoryBound(to: Int.self) + count.pointee += 1 + return count.pointee < 2 + }) + #expect(count == 2) + } + + @Test(.enabled(if: referencePathGeometryEnabled), arguments: [false, true]) + func flattenedCurvesPreserveReferenceStepsAndLength(quadratic: Bool) { + let path = curve(quadratic: quadratic) + defer { path.release() } + var elements: [PathElementRecord] = [] + let completed = withUnsafeMutablePointer(to: &elements) { + path.applyLines(tolerance: 0.5, info: $0) { info, element, point in + let points: [CGFloat] = element.rawValue == 2 ? [] : [point.pointee.x, point.pointee.y] + info.assumingMemoryBound(to: [PathElementRecord].self).pointee.append( + PathElementRecord(element: element.rawValue, points: points) + ) + return true + } + } + #expect(completed) + let expected: [CGFloat] = quadratic + ? [0.1, 0.2, 2.5562498569488525, 3.8625001907348633, 5.0249996185302734, 5.0500001907348633, + 7.5062494277954102, 3.762500524520874, 10, 0] + : [0.1, 0.2, 0.79861116409301758, 4.282407283782959, 2.6222224235534668, 6.7259254455566406, + 5.0125002861022949, 7.5249991416931152, 7.4111113548278809, 6.6740727424621582, + 9.2597227096557617, 4.1675910949707031, 10, 0] + #expect(elements.map(\.element) == [0] + Array(repeating: 1, count: quadratic ? 4 : 6) + [2]) + #expect(elements.flatMap(\.points) == expected) + let expectedLength = quadratic ? 24.360695925004599 : 29.409184003419121 + #expect(abs(path.length(tolerance: 0.5, transform: nil) - expectedLength) < 1e-10) + var transform = CGAffineTransform(a: 2, b: 0, c: 0, d: 3, tx: 7, ty: 11) + let transformedLength = quadratic ? 56.859833567655286 : 72.313997446559654 + #expect(abs(path.length(tolerance: 0.5, transform: &transform) - transformedLength) < 1e-10) + } + + @Test(arguments: [false, true]) + func applyPreservesRawMoveAndLineCommands(hasInitialMove: Bool) { + withPathStorage { storage in + if hasInitialMove { + storage.append(element: .moveToPoint, points: [1, 2], userInfo: nil) + storage.append(element: .moveToPoint, points: [3, 4], userInfo: nil) + storage.append(element: .addLineToPoint, points: [3, 4], userInfo: nil) + storage.append(element: .closeSubpath, points: [], userInfo: nil) + storage.append(element: .closeSubpath, points: [], userInfo: nil) + } else { + storage.append(element: .addLineToPoint, points: [3, 4], userInfo: nil) + storage.append(element: .addLineToPoint, points: [5, 6], userInfo: nil) + } + withStoragePath(storage) { path in + var kinds: [Int32] = [] + let completed = withUnsafeMutablePointer(to: &kinds) { + path.applyCGPathElements(transform: nil, info: $0) { info, element in + info.assumingMemoryBound(to: [Int32].self).pointee.append(element.pointee.type.rawValue) + return true + } + } + #expect(completed) + #expect(kinds == (hasInitialMove ? [0, 0, 1, 4] : [1, 1])) + kinds.removeAll() + let linesCompleted = withUnsafeMutablePointer(to: &kinds) { + path.applyLines(tolerance: 0.5, info: $0) { info, element, _ in + info.assumingMemoryBound(to: [Int32].self).pointee.append(element.rawValue) + return true + } + } + #expect(linesCompleted) + #expect(kinds == (hasInitialMove ? [0, 0, 1, 2] : [1, 1])) + } + } + } + + @Test + func closedLineLengthIncludesClosingEdge() { + let cgPath = CGMutablePath() + cgPath.move(to: .zero) + cgPath.addLine(to: CGPoint(x: 3, y: 4)) + cgPath.closeSubpath() + let path = ORBPath(cgPath: cgPath) + defer { path.release() } + #expect(path.length(tolerance: 0.5, transform: nil) == 10) + var transform = CGAffineTransform(scaleX: 2, y: 3) + #expect(abs(path.length(tolerance: 0.5, transform: &transform) - 26.83281572999748) < 1e-12) + } + + @Test + func interpolationPromotesRoundedRectAndPreservesEndpoints() { + let lhs = ORBPath(rect: CGRect(x: 1, y: 2, width: 10, height: 20), transform: nil) + let rhs = ORBPath(roundedRect: CGRect(x: 3, y: 4, width: 30, height: 40), cornerWidth: 5, cornerHeight: 6, style: .continuous, transform: nil) + defer { lhs.release(); rhs.release() } + #expect(lhs.canInterpolate(to: rhs)) + let mixed = lhs.interpolated(to: rhs, fraction: 0.25) + defer { mixed.release() } + #expect(pathElements(mixed) == [PathElementRecord(element: 9, points: [1.5, 2.5, 15, 25, 1.25, 1.5])]) + for fraction: Float in [0, 1] { + let endpoint = lhs.interpolated(to: rhs, fraction: fraction) + defer { endpoint.release() } + let expected = fraction == 0 ? lhs : rhs + #expect(endpoint.storage == expected.storage) + #expect(endpoint.callbacks == expected.callbacks) + } + let ellipse = ORBPath(ellipseIn: CGRect(x: 0, y: 0, width: 10, height: 10), transform: nil) + defer { ellipse.release() } + #expect(!lhs.canInterpolate(to: ellipse)) + let incompatible = lhs.interpolated(to: ellipse, fraction: 0.25) + defer { incompatible.release() } + #expect(unsafeBitCast(incompatible.storage, to: UInt.self) == 1) + } + + @Test + func interpolatedStrokeOwnsMetadataAndChild() { + func makeStroke(rect: CGRect, width: CGFloat, cap: Int32, dash: [CGFloat]) -> ORBPath { + let source = ORBPath(rect: rect, transform: nil) + defer { source.release() } + return dash.withUnsafeBufferPointer { + source.stroked(lineWidth: width, lineCap: cap, lineJoin: 1, miterLimit: 10, dashPhase: 0, dash: $0.baseAddress, dashCount: $0.count) + } + } + let lhs = makeStroke(rect: CGRect(x: 1, y: 2, width: 10, height: 20), width: 1, cap: 0, dash: [2, 4]) + let rhs = makeStroke(rect: CGRect(x: 3, y: 4, width: 30, height: 40), width: 3, cap: 2, dash: [3, 5, 7]) + #expect(lhs.canInterpolate(to: rhs)) + let path = lhs.interpolated(to: rhs, fraction: 0.25) + lhs.release() + rhs.release() + struct Result { var width: CGFloat = 0; var cap: Int32 = -1; var dash: [CGFloat] = []; var child = ORBPath.empty } + var result = Result() + #expect(path.applyShape(allowLine: true, info: &result) { info, type, shape in + if type == .stroked { + let stroke = shape.pointee.stroked + let result = info.assumingMemoryBound(to: Result.self) + result.pointee.width = stroke.style.lineWidth + result.pointee.cap = stroke.style.lineCap + result.pointee.dash = Array(UnsafeBufferPointer(start: stroke.style.dash, count: Int(stroke.style.dashCount))) + result.pointee.child = stroke.path.retain() + } + }) + path.release() + defer { result.child.release() } + #expect(result.width == 1.5) + #expect(result.cap == 2) + #expect(result.dash == [3, 5, 7]) + #expect(result.child.boundingRect == CGRect(x: 1.5, y: 2.5, width: 15, height: 25)) + } + + @Test + func serializedRectUsesCompatibleWireFormat() { + let path = ORBPath(rect: CGRect(x: 1, y: 2, width: 10, height: 20), transform: nil) + defer { path.release() } + let data = path.data as Data + #expect(data == Data([0x1a, 1, 5, 0x12, 0x10, 0, 0, 0x80, 0x3f, 0, 0, 0, 0x40, 0, 0, 0x20, 0x41, 0, 0, 0xa0, 0x41])) + let copy = ORBPath(data: data as CFData) + defer { copy.release() } + #expect(copy.isEqual(to: path)) + #expect(path.debugDescription as String == "RBPath:\n rect 1 2 10 20\n") + let fractional = ORBPath(rect: CGRect(x: 0.1, y: 0.2, width: 10.3, height: 20.4), transform: nil) + let decoded = ORBPath(data: fractional.data) + fractional.release() + defer { decoded.release() } + #expect(pathElements(decoded) == [PathElementRecord(element: 5, points: [CGFloat(Float(0.1)), CGFloat(Float(0.2)), CGFloat(Float(10.3)), CGFloat(Float(20.4))])]) + } + + @Test(arguments: [ + ([UInt8](), false), ([0x80], false), ([0x0f], false), + ([0x1a, 1, 0], true), ([0x1a, 1, 0x10], true), ([0x1a, 1, 0x91], true), + ]) + func dataDecoderDistinguishesEmptyMessagesFromInvalidPaths(bytes: [UInt8], isNull: Bool) { + let path = ORBPath(data: Data(bytes) as CFData) + defer { path.release() } + #expect(path.isEmpty) + #expect((unsafeBitCast(path.storage, to: UInt.self) == 1) == isNull) + if !isNull { + #expect(unsafeBitCast(path.storage, to: UInt.self) > 1) + } + } +} +#endif diff --git a/Tests/OpenRenderBoxCompatibilityTests/PathRegressionTests.swift b/Tests/OpenRenderBoxCompatibilityTests/PathRegressionTests.swift new file mode 100644 index 0000000..c8c2e92 --- /dev/null +++ b/Tests/OpenRenderBoxCompatibilityTests/PathRegressionTests.swift @@ -0,0 +1,242 @@ +// +// PathRegressionTests.swift +// OpenRenderBoxCompatibilityTests + +import Testing + +#if canImport(CoreGraphics) && (OPENRENDERBOX_CF_CGTYPES || OPENRENDERBOX_COMPATIBILITY_TEST) +import CoreGraphics + +@Suite +struct PathRegressionTests { + private struct ElementSource { + var element: ORBPath.Element + var points: UnsafePointer + } + + private func withElementPath( + element: ORBPath.Element, + points: [CGFloat], + _ body: (ORBPath) throws -> Result + ) rethrows -> Result { + try points.withUnsafeBufferPointer { points in + var source = ElementSource(element: element, points: points.baseAddress!) + var callbacks = ORBPath.Callbacks() + callbacks.apply = { object, info, callback in + let source = object.assumingMemoryBound(to: ElementSource.self).pointee + return callback?(info, source.element, source.points, nil) ?? true + } + callbacks.isSingleElement = { _ in true } + return try withUnsafePointer(to: &source) { source in + try withUnsafePointer(to: &callbacks) { callbacks in + let path = ORBPath(storage: unsafeBitCast(source, to: ORBPath.Storage.self), callbacks: callbacks) + return try body(path) + } + } + } + } + + @Test(arguments: [ORBPath.Element.moveToPoint, .addLineToPoint, .closeSubpath]) + func shapeRecognitionAcceptsMinimalPayloads(element: ORBPath.Element) { + let values: [CGFloat] = element == .closeSubpath ? [0] : [1, 2] + withElementPath(element: element, points: values) { path in + var callbackCount = 0 + let recognized = path.applyShape(allowLine: true, info: &callbackCount) { info, _, _ in + info.assumingMemoryBound(to: Int.self).pointee += 1 + } + #expect(!recognized) + #expect(callbackCount == 0) + } + } + + @Test + func retainUsesReturnedStorage() { + var destination: UInt64 = 0 + withUnsafePointer(to: &destination) { destination in + var source = UnsafeRawPointer(destination) + var callbacks = ORBPath.Callbacks() + callbacks.retain = { object in + object.assumingMemoryBound(to: UnsafeRawPointer.self).pointee + } + withUnsafePointer(to: &source) { source in + withUnsafePointer(to: &callbacks) { callbacks in + let path = ORBPath(storage: unsafeBitCast(source, to: ORBPath.Storage.self), callbacks: callbacks) + let retained = path.retain() + #expect(unsafeBitCast(retained.storage, to: UnsafeRawPointer.self) == UnsafeRawPointer(destination)) + #expect(retained.callbacks == callbacks) + } + } + } + } + + @Test + func batchContainmentPreservesAllMaskBits() { + let path = ORBPath(rect: CGRect(x: 0, y: 0, width: 10, height: 10), transform: nil) + defer { path.release() } + var points = Array(repeating: CGPoint(x: 20, y: 20), count: 65) + points[0] = CGPoint(x: 5, y: 5) + points[63] = CGPoint(x: 5, y: 5) + points[64] = CGPoint(x: 5, y: 5) + #expect(path.containsPoints(count: 0, points: points, eoFill: false, transform: nil) == 0) + #expect(path.containsPoints(count: 65, points: points, eoFill: false, transform: nil) == (UInt64(1) | (UInt64(1) << 63))) + } + + @Test + func batchContainmentTransformsQueryPoints() { + let path = ORBPath(rect: CGRect(x: 0, y: 0, width: 10, height: 10), transform: nil) + defer { path.release() } + var transform = CGAffineTransform(translationX: -10, y: -20) + let points = [CGPoint(x: 15, y: 25), CGPoint(x: 5, y: 5)] + #expect(path.containsPoints(count: 2, points: points, eoFill: false, transform: &transform) & 3 == 1) + } + + @Test + func containmentUsesTheRequestedWindingRule() { + let cgPath = CGMutablePath() + cgPath.addRect(CGRect(x: 0, y: 0, width: 20, height: 20)) + cgPath.addRect(CGRect(x: 5, y: 5, width: 10, height: 10)) + let path = ORBPath(cgPath: cgPath) + defer { path.release() } + #expect(path.contains(point: CGPoint(x: 10, y: 10), eoFill: false)) + #expect(!path.contains(point: CGPoint(x: 10, y: 10), eoFill: true)) + #expect(path.contains(point: CGPoint(x: 2, y: 2), eoFill: true)) + } + + @Test + func nullPrimitiveRectanglesUseEmptyStorage() { + let paths = [ + ORBPath(rect: .null, transform: nil), + ORBPath(ellipseIn: .null, transform: nil), + ORBPath(roundedRect: .null, cornerWidth: 4, cornerHeight: 5, style: .circular, transform: nil), + ORBPath(roundedRect: .null, topLeftRadius: 1, topRightRadius: 2, bottomRightRadius: 3, bottomLeftRadius: 4, style: .continuous, transform: nil), + ] + for path in paths { + #expect(unsafeBitCast(path.storage, to: UInt.self) == 0) + #expect(path.isEmpty) + path.release() + } + let null = ORBPath(cgPath: nil) + #expect(unsafeBitCast(null.storage, to: UInt.self) == 1) + null.release() + } + + @Test + func sentinelEqualityPreservesNullMarker() { + #expect(ORBPath.empty.isEqual(to: .null)) + #expect(ORBPath.null.isEqual(to: .empty)) + var callbacks = ORBPath.Callbacks() + callbacks.apply = { _, _, _ in true } + withUnsafePointer(to: &callbacks) { callbacks in + var payload: UInt64 = 0 + withUnsafePointer(to: &payload) { payload in + let custom = ORBPath(storage: unsafeBitCast(payload, to: ORBPath.Storage.self), callbacks: callbacks) + #expect(ORBPath.empty.isEqual(to: custom)) + #expect(custom.isEqual(to: .empty)) + #expect(!ORBPath.null.isEqual(to: custom)) + #expect(!custom.isEqual(to: .null)) + } + } + } + + @Test(arguments: [ + (CGPoint(x: 5, y: 0), false), + (CGPoint(x: 10, y: 5), false), + (CGPoint(x: 5, y: 10), false), + (CGPoint(x: 0, y: 5), true), + (CGPoint(x: 5, y: 5), true), + ]) + func ellipseBoundaryContainment(point: CGPoint, expected: Bool) { + let path = ORBPath(ellipseIn: CGRect(x: 0, y: 0, width: 10, height: 10), transform: nil) + defer { path.release() } + #expect(path.contains(point: point, eoFill: false) == expected) + } + + @Test + func equalityFallsBackToElementContents() { + let values: [CGFloat] = [1, 2, 30, 40] + withElementPath(element: .rect, points: values) { lhs in + withElementPath(element: .rect, points: values) { rhs in + #expect(lhs.callbacks != rhs.callbacks) + #expect(lhs.isEqual(to: rhs)) + let sameCallbacks = ORBPath(storage: rhs.storage, callbacks: lhs.callbacks) + #expect(lhs.isEqual(to: sameCallbacks)) + } + withElementPath(element: .rect, points: [1, 2, 30, 41]) { rhs in + #expect(!lhs.isEqual(to: rhs)) + } + withElementPath(element: .ellipse, points: values) { rhs in + #expect(!lhs.isEqual(to: rhs)) + } + } + } + + @Test + func genericEqualityUsesCompactStoragePrecision() { + let values: [CGFloat] = [0.1, 0.2, 30.3, 40.4] + let compactValues = values.map { CGFloat(Float($0)) } + withElementPath(element: .rect, points: values) { original in + withElementPath(element: .rect, points: compactValues) { compact in + #expect(original.isEqual(to: compact)) + #expect(!compact.isEqual(to: original)) + } + withElementPath(element: .rect, points: values) { other in + #expect(!original.isEqual(to: other)) + } + } + } + + @Test(arguments: [ORBPath.RoundedCornerStyle.circular, .continuous]) + func unevenRoundedRectanglePreservesPrimitiveStyle(style: ORBPath.RoundedCornerStyle) { + let rect = CGRect(x: 1, y: 2, width: 100, height: 80) + let path = ORBPath(roundedRect: rect, topLeftRadius: 5, topRightRadius: 10, bottomRightRadius: 15, bottomLeftRadius: 20, style: style, transform: nil) + defer { path.release() } + let elements = pathElements(path) + #expect(elements.count == 1) + #expect(elements.first?.element == (style == .circular ? ORBPath.Element.unevenRoundedRectCircular.rawValue : ORBPath.Element.unevenRoundedRectContinuous.rawValue)) + #expect(path.boundingRect == rect) + #expect(path.contains(point: CGPoint(x: 50, y: 40), eoFill: false)) + #expect(!path.contains(point: CGPoint(x: 1.01, y: 2.01), eoFill: false)) + var actualStyle = ORBPath.RoundedCornerStyle.circular + var actualType = ORBPathShapeType.rect + #expect(path.applyShape(allowLine: true, info: &actualStyle) { info, type, shape in + if type == .unevenRoundedRect { + info.assumingMemoryBound(to: ORBPath.RoundedCornerStyle.self).pointee = shape.pointee.unevenRoundedRect.style + } + }) + #expect(actualStyle == style) + #expect(path.applyShape(allowLine: true, info: &actualType) { info, type, _ in + info.assumingMemoryBound(to: ORBPathShapeType.self).pointee = type + }) + #expect(actualType == .unevenRoundedRect) + } + + @Test + func continuousRoundedRectangleGeometry() { + let path = ORBPath(roundedRect: CGRect(x: 0, y: 0, width: 100, height: 80), cornerWidth: 10, cornerHeight: 20, style: .continuous, transform: nil) + defer { path.release() } + let elements = cgPathElements(path.cgPath) + #expect(elements.count == 18) + #expect(elements.prefix(3).map(\.element) == [0, 1, 3]) + let actual = elements.prefix(3).flatMap(\.points) + let expected: [CGFloat] = [100, 40, 100, 49.426701068878174, + 100, 58.230199813842773, 100, 62.63185977935791, + 99.250885993242264, 67.370120286941528] + #expect(actual.count == expected.count) + for (actual, expected) in zip(actual, expected) { + #expect(abs(actual - expected) < 1e-10) + } + } + + @Test + func unevenRoundedRectangleStylesHaveDifferentGeometry() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 80) + let circular = ORBPath(roundedRect: rect, topLeftRadius: 5, topRightRadius: 10, bottomRightRadius: 15, bottomLeftRadius: 20, style: .circular, transform: nil) + let continuous = ORBPath(roundedRect: rect, topLeftRadius: 5, topRightRadius: 10, bottomRightRadius: 15, bottomLeftRadius: 20, style: .continuous, transform: nil) + defer { + circular.release() + continuous.release() + } + #expect(circular.cgPath != continuous.cgPath) + } +} +#endif diff --git a/Tests/OpenRenderBoxCompatibilityTests/PathStorageTests.swift b/Tests/OpenRenderBoxCompatibilityTests/PathStorageTests.swift index 4883f1b..7f66142 100644 --- a/Tests/OpenRenderBoxCompatibilityTests/PathStorageTests.swift +++ b/Tests/OpenRenderBoxCompatibilityTests/PathStorageTests.swift @@ -4,57 +4,76 @@ import Testing -#if canImport(CoreGraphics) +#if canImport(CoreGraphics) && (OPENRENDERBOX_CF_CGTYPES || OPENRENDERBOX_COMPATIBILITY_TEST) import CoreGraphics -@Suite(.enabled(if: compatibilityTestEnabled)) +@Suite struct PathStorageTests { + private struct ClipKeyframe: Equatable { + var tag: Int32 + var value: Float + var start: Float + var end: Float + } + + private struct ClipInfo { + var flags: Int32 + var cap: Int32 + var count: Int + var keys: UnsafePointer? + } + @Test func storageIsEmpty() { let path = ORBPath(rect: CGRect(x: 0, y: 0, width: 100, height: 100), transform: nil) - let storage = path.storage - storage.initialize(capacity: 96, source: nil) - #expect(storage.isEmpty == true) - storage.append(path: path) - #expect(storage.isEmpty == false) - storage.destroy() - path.release() + defer { path.release() } + withPathStorage { storage in + #expect(storage.isEmpty) + #expect(storage.append(path: path)) + #expect(!storage.isEmpty) + storage.clear() + #expect(storage.isEmpty) + #expect(pathElements(storage).isEmpty) + } } @Test func storageIsSingleElement() { let path = ORBPath(rect: CGRect(x: 0, y: 0, width: 100, height: 100), transform: nil) - let storage = path.storage - storage.initialize(capacity: 96, source: nil) - #expect(storage.isSingleElement == false) - storage.append(path: path) - #expect(storage.isSingleElement == true) - storage.destroy() - path.release() + defer { path.release() } + withPathStorage { storage in + #expect(!storage.isSingleElement) + #expect(storage.append(path: path)) + #expect(storage.isSingleElement) + #expect(storage.append(path: path)) + #expect(!storage.isSingleElement) + } } @Test func storageBezierOrder() { - let path = ORBPath(rect: CGRect(x: 0, y: 0, width: 100, height: 100), transform: nil) - let order = path.storage.bezierOrder - #expect(order == 1) - path.release() + withPathStorage { storage in + #expect(storage.bezierOrder == 1) + storage.append(element: .moveToPoint, points: [0, 0], userInfo: nil) + storage.append(element: .addQuadCurveToPoint, points: [1, 2, 3, 4], userInfo: nil) + #expect(storage.bezierOrder == 2) + storage.append(element: .addCurveToPoint, points: [5, 6, 7, 8, 9, 10], userInfo: nil) + #expect(storage.bezierOrder == 3) + storage.clear() + #expect(storage.bezierOrder == 1) + } } @Test func storageBoundingRect() { let rect = CGRect(x: 10, y: 20, width: 100, height: 50) let path = ORBPath(rect: rect, transform: nil) - let storage = path.storage - storage.initialize(capacity: 96, source: nil) - storage.append(path: path) - let boundingRect = path.storage.boundingRect - #expect(boundingRect.origin.x == rect.origin.x) - #expect(boundingRect.origin.y == rect.origin.y) - #expect(boundingRect.size.width == rect.size.width) - #expect(boundingRect.size.height == rect.size.height) - storage.destroy() - path.release() + defer { path.release() } + withPathStorage { storage in + #expect(storage.append(path: path)) + #expect(storage.boundingRect == rect) + #expect(pathElements(storage) == pathElements(path)) + } } @Test(arguments: [ @@ -65,40 +84,339 @@ struct PathStorageTests { func storageEquality(rect1: CGRect, rect2: CGRect, expectedEqual: Bool) { let path1 = ORBPath(rect: rect1, transform: nil) let path2 = ORBPath(rect: rect2, transform: nil) - let storage1 = path1.storage - let storage2 = path2.storage - storage1.initialize(capacity: 96, source: nil) - storage1.append(path: path1) - storage2.initialize(capacity: 96, source: nil) - storage2.append(path: path2) - #expect(storage1.isEqual(to: storage2) == expectedEqual) - storage1.destroy() - storage2.destroy() - path1.release() - path2.release() - } - - @Test("Verify no crash or memleak of ORBPathStorageGetCGPath call", arguments: 1...20) - func storageGetCGPath(iteration: Int) { - let rect = CGRect(x: 0, y: 0, width: 100, height: 100) - let path = ORBPath(rect: rect, transform: nil) - let storage = path.storage - storage.initialize(capacity: 64, source: nil) - storage.append(path: path) - for _ in 1...7 { - storage.append(element: .moveToPoint, points: [50, 50], userInfo: nil) - storage.append(element: .addLineToPoint, points: [100, 50], userInfo: nil) - storage.append(element: .closeSubpath, points: [], userInfo: nil) + defer { + path1.release() + path2.release() } - if let p = storage.cgPath { - _ = p.isEmpty - _ = p.boundingBox + withPathStorage { storage1 in + withPathStorage { storage2 in + #expect(storage1.append(path: path1)) + #expect(storage2.append(path: path2)) + #expect(storage1.isEqual(to: storage2) == expectedEqual) + } } - storage.destroy() - path.release() } -} + @Test(arguments: [64, 256], [1, 40]) + func storageCopyPreservesElements(capacity: Int, lineCount: Int) { + withPathStorage(capacity: capacity) { source in + source.append(element: .moveToPoint, points: [1, 2], userInfo: nil) + for index in 1...lineCount { + source.append(element: .addLineToPoint, points: [CGFloat(index), CGFloat(index * 2)], userInfo: nil) + } + let expected = pathElements(source) + for destinationCapacity in [64, 256] { + withPathStorage(capacity: destinationCapacity, source: source) { copy in + #expect(pathElements(copy) == expected) + #expect(copy.isEqual(to: source)) + copy.append(element: .addLineToPoint, points: [100, 200], userInfo: nil) + #expect(pathElements(source) == expected) + } + } + withPathStorage(capacity: 64, source: source) { copy in + source.clear() + #expect(pathElements(copy) == expected) + } + } + } -#endif + @Test + func appendingStorageToItselfUsesSnapshot() { + withPathStorage { storage in + storage.append(element: .moveToPoint, points: [1, 2], userInfo: nil) + storage.append(element: .addLineToPoint, points: [3, 4], userInfo: nil) + let original = pathElements(storage) + withStoragePath(storage) { path in + #expect(storage.append(path: path)) + } + #expect(pathElements(storage) == original + original) + } + } + + @Test + func storageOwnsClipStrokeKeyframes() { + let expected = [ + ClipKeyframe(tag: 0, value: 0, start: 0, end: 0.5), + ClipKeyframe(tag: 0, value: 1, start: 0.5, end: 1), + ] + #expect(MemoryLayout.size == 16) + #expect(MemoryLayout.size == 24) + withPathStorage(capacity: 64) { storage in + var keys = expected + keys.withUnsafeBufferPointer { keys in + var metadata = ClipInfo(flags: 2, cap: 1, count: keys.count, keys: keys.baseAddress) + withUnsafePointer(to: &metadata) { metadata in + storage.append(element: .clipStroke, points: [0.25], userInfo: metadata) + } + } + keys[0].value = 42 + keys.removeAll() + storage.append(element: .rect, points: [0, 0, 10, 10], userInfo: nil) + storage.append(element: .end, points: [], userInfo: nil) + withPathStorage(capacity: 64, source: storage) { copy in + storage.clear() + var result: [ClipKeyframe] = [] + withUnsafeMutablePointer(to: &result) { result in + _ = copy.apply(info: result) { info, element, _, metadata in + if element == .clipStroke, let metadata { + let clip = metadata.assumingMemoryBound(to: ClipInfo.self).pointee + info.assumingMemoryBound(to: [ClipKeyframe].self).pointee = Array(UnsafeBufferPointer(start: clip.keys, count: clip.count)) + } + return true + } + } + #expect(result == expected) + } + } + } + + @Test(arguments: [CGFloat(0), 0.25, 1]) + func clippedStrokeUsesProgressAndRoundCaps(progress: CGFloat) { + let keys = [ + ClipKeyframe(tag: 0, value: 0, start: 0, end: 0.5), + ClipKeyframe(tag: 0, value: 1, start: 0.5, end: 1), + ] + withPathStorage { storage in + keys.withUnsafeBufferPointer { keys in + var metadata = ClipInfo(flags: 0, cap: 1, count: keys.count, keys: keys.baseAddress) + withUnsafePointer(to: &metadata) { metadata in + storage.append(element: .clipStroke, points: [progress], userInfo: metadata) + } + } + storage.append(element: .rect, points: [0, 0, 10, 10], userInfo: nil) + storage.append(element: .end, points: [], userInfo: nil) + withStoragePath(storage) { path in + let elements = cgPathElements(path.cgPath) + if progress == 0 { + #expect(elements.isEmpty) + } else if progress == 1 { + #expect(elements == [ + PathElementRecord(element: 0, points: [0, 0]), + PathElementRecord(element: 1, points: [10, 0]), + PathElementRecord(element: 1, points: [10, 10]), + PathElementRecord(element: 1, points: [0, 10]), + PathElementRecord(element: 4, points: []), + ]) + } else { + #expect(elements == [ + PathElementRecord(element: 0, points: [0, 0]), + PathElementRecord(element: 1, points: [5, 0]), + PathElementRecord(element: 3, points: [5, -1.3807119131088257, 6.1192879676818848, -2.5, 7.5, -2.5]), + PathElementRecord(element: 3, points: [8.880711555480957, -2.5, 10, -1.3807119131088257, 10, 0]), + PathElementRecord(element: 1, points: [10, 5]), + PathElementRecord(element: 1, points: [10, 10]), + PathElementRecord(element: 1, points: [5, 10]), + PathElementRecord(element: 3, points: [5, 11.380711555480957, 3.8807117938995361, 12.5, 2.5, 12.5]), + PathElementRecord(element: 3, points: [1.1192880868911743, 12.5, 0, 11.380711555480957, 0, 10]), + PathElementRecord(element: 1, points: [0, 5]), + PathElementRecord(element: 1, points: [0, 0]), + PathElementRecord(element: 4, points: []), + ]) + } + } + } + } + + @Test + func clippedStrokeUsesCustomKeyframeCap() { + let keys = [ + ClipKeyframe(tag: 2, value: 0, start: 0, end: 0.25), + ClipKeyframe(tag: 0, value: 1, start: 0.5, end: 0.75), + ] + expectClippedRectangle(keys: keys, progress: 0.25, expected: [ + PathElementRecord(element: 0, points: [0, 0]), + PathElementRecord(element: 1, points: [10, 0]), + PathElementRecord(element: 1, points: [10, 2.5]), + PathElementRecord(element: 1, points: [0, 2.5]), + PathElementRecord(element: 4, points: []), + ]) + } + @Test + func clippedStrokeWrapsTaggedKeyframe() { + let keys = [ + ClipKeyframe(tag: 0, value: 0, start: 0, end: 0.1), + ClipKeyframe(tag: 1, value: 0.5, start: 0.8, end: 0.7), + ClipKeyframe(tag: 0, value: 1, start: 0.5, end: 0.6), + ] + expectClippedRectangle(keys: keys, progress: 0.75, expected: [ + PathElementRecord(element: 0, points: [4, 10]), + PathElementRecord(element: 1, points: [0, 10]), + PathElementRecord(element: 1, points: [0, 8]), + PathElementRecord(element: 1, points: [5, 9]), + PathElementRecord(element: 3, points: [5.3186259269714355, 9.0637245178222656, 5.525263786315918, 9.3736820220947266, 5.4615383148193359, 9.6923074722290039]), + PathElementRecord(element: 3, points: [5.3978133201599121, 10.010933876037598, 5.0878567695617676, 10.217571258544922, 4.769230842590332, 10.15384578704834]), + PathElementRecord(element: 4, points: []), + PathElementRecord(element: 0, points: [0, 0]), + PathElementRecord(element: 1, points: [4, 0]), + PathElementRecord(element: 1, points: [2, 10]), + PathElementRecord(element: 1, points: [0, 10]), + PathElementRecord(element: 4, points: []), + ]) + } + + @Test(.enabled(if: referencePathGeometryEnabled)) + func clippedStrokeExtendsBelowProgressRange() { + let keys = [ + ClipKeyframe(tag: 0, value: 0, start: 0, end: 0.5), + ClipKeyframe(tag: 0, value: 1, start: 0.5, end: 1), + ] + expectClippedRectangle(keys: keys, flags: 4, progress: -0.25, expected: [ + PathElementRecord(element: 0, points: [8.821488380432129, 11.178511619567871]), + PathElementRecord(element: 1, points: [-1.178511381149292, 11.178511619567871]), + PathElementRecord(element: 1, points: [-1.178511381149292, 1.178511381149292]), + PathElementRecord(element: 1, points: [8.821488380432129, 1.178511381149292]), + PathElementRecord(element: 1, points: [8.821488380432129, 11.178511619567871]), + PathElementRecord(element: 1, points: [-1.178511381149292, 11.178511619567871]), + PathElementRecord(element: 1, points: [-1.178511381149292, 1.178511381149292]), + PathElementRecord(element: 1, points: [8.821488380432129, 1.178511381149292]), + PathElementRecord(element: 3, points: [11.582912445068359, 1.178511381149292, 13.821488380432129, 3.4170875549316406, 13.821488380432129, 6.178511142730713]), + PathElementRecord(element: 3, points: [13.821488380432129, 8.939934730529785, 11.582912445068359, 11.178511619567871, 8.821488380432129, 11.178511619567871]), + PathElementRecord(element: 4, points: []), + ]) + } + + private func expectClippedRectangle( + keys: [ClipKeyframe], flags: Int32 = 0, progress: CGFloat, expected: [PathElementRecord] + ) { + withPathStorage { storage in + keys.withUnsafeBufferPointer { keys in + var metadata = ClipInfo(flags: flags, cap: 1, count: keys.count, keys: keys.baseAddress) + withUnsafePointer(to: &metadata) { metadata in + storage.append(element: .clipStroke, points: [progress], userInfo: metadata) + } + } + storage.append(element: .rect, points: [0, 0, 10, 10], userInfo: nil) + storage.append(element: .end, points: [], userInfo: nil) + withStoragePath(storage) { path in + let actual = normalizedClipElements(cgPathElements(path.cgPath)) + #expect(actual.map(\.element) == expected.map(\.element)) + for (actual, expected) in zip(actual, expected) { + #expect(actual.points.count == expected.points.count) + for (actual, expected) in zip(actual.points, expected.points) { + #expect(abs(actual - expected) < 1e-6) + } + } + } + } + } + + @Test + func glyphDilationPreservesClosedGeometry() { + withPathStorage { storage in + storage.append(element: .dilateGlyph, points: [2, 3], userInfo: nil) + storage.append(element: .rect, points: [0, 0, 10, 10], userInfo: nil) + storage.append(element: .end, points: [], userInfo: nil) + withStoragePath(storage) { path in + let actual = cgPathElements(path.cgPath) + #expect(actual == [ + PathElementRecord(element: 0, points: [0, 0]), + PathElementRecord(element: 1, points: [10, 0]), + PathElementRecord(element: 1, points: [12, 3]), + PathElementRecord(element: 1, points: [12, 13]), + PathElementRecord(element: 1, points: [10, 16]), + PathElementRecord(element: 1, points: [0, 16]), + PathElementRecord(element: 1, points: [-2, 13]), + PathElementRecord(element: 1, points: [-2, 3]), + PathElementRecord(element: 4, points: []), + ]) + } + } + } + + @Test(arguments: [CGFloat(2), -2]) + func offsetCurveUsesMappedStorage(amount: CGFloat) { + withPathStorage(capacity: 64) { storage in + storage.append(element: .offset, points: [amount], userInfo: nil) + storage.append(element: .moveToPoint, points: [10, 20], userInfo: nil) + storage.append(element: .addCurveToPoint, points: [10, 30, 20, 30, 20, 20], userInfo: nil) + storage.append(element: .end, points: [], userInfo: nil) + withStoragePath(storage) { path in + let cgPath = path.cgPath + let expected: [PathElementRecord] = amount > 0 + ? [PathElementRecord(element: 0, points: [8, 20]), + PathElementRecord(element: 3, points: [8, 32.666667938232422, 22, 32.666667938232422, 22, 20])] + : [PathElementRecord(element: 0, points: [12, 20]), + PathElementRecord(element: 3, points: [12, 27.333333969116211, 18, 27.333333969116211, 18, 20])] + #expect(cgPathElements(cgPath) == expected) + storage.append(element: .rect, points: [100, 100, 10, 10], userInfo: nil) + #expect(path.cgPath.boundingBoxOfPath.maxX == 110) + #expect(cgPathElements(cgPath) == expected) + } + } + } + + @Test(arguments: [ORBPath.Element.boolean, .offset, .transformed]) + func nestedBooleanPreservesPathBoundaries(inner: ORBPath.Element) { + withPathStorage { storage in + let intersection: [UInt8] = [1, 0] + intersection.withUnsafeBufferPointer { flags in + storage.append(element: .boolean, points: [], userInfo: flags.baseAddress) + } + if inner == .boolean { + let normalize: [UInt8] = [0, 0] + normalize.withUnsafeBufferPointer { flags in + storage.append(element: .boolean, points: [], userInfo: flags.baseAddress) + } + } else if inner == .offset { + storage.append(element: .offset, points: [0], userInfo: nil) + } else { + var transform = CGAffineTransform.identity + withUnsafePointer(to: &transform) { transform in + storage.append(element: .transformed, points: [], userInfo: transform) + } + } + storage.append(element: .rect, points: [0, 0, 10, 10], userInfo: nil) + storage.append(element: .end, points: [], userInfo: nil) + storage.append(element: .rect, points: [5, 5, 10, 10], userInfo: nil) + storage.append(element: .end, points: [], userInfo: nil) + withStoragePath(storage) { path in + let cgPath = path.cgPath + let coalescesPaths = inner == .transformed + #expect(cgPath.boundingBoxOfPath == (coalescesPaths + ? CGRect(x: 0, y: 0, width: 15, height: 15) + : CGRect(x: 5, y: 5, width: 5, height: 5))) + #expect(cgPath.contains(CGPoint(x: 7, y: 7))) + #expect(cgPath.contains(CGPoint(x: 2, y: 2)) == coalescesPaths) + #expect(cgPath.contains(CGPoint(x: 12, y: 12)) == coalescesPaths) + } + } + } + + @Test + func storageStopsEnumeration() { + withPathStorage { storage in + storage.append(element: .moveToPoint, points: [1, 2], userInfo: nil) + storage.append(element: .addLineToPoint, points: [3, 4], userInfo: nil) + var count = 0 + let completed = storage.apply(info: &count) { info, _, _, _ in + info.assumingMemoryBound(to: Int.self).pointee += 1 + return false + } + #expect(!completed) + #expect(count == 1) + } + } + + @Test("Cached paths follow storage mutation", arguments: 1...4) + func storageGetCGPath(iteration: Int) throws { + let rect = CGRect(x: 0, y: 0, width: 100, height: 100) + let path = ORBPath(rect: rect, transform: nil) + defer { path.release() } + try withPathStorage(capacity: 64) { storage in + #expect(storage.append(path: path)) + for _ in 1...7 { + storage.append(element: .moveToPoint, points: [50, 50], userInfo: nil) + storage.append(element: .addLineToPoint, points: [100, 50], userInfo: nil) + storage.append(element: .closeSubpath, points: [], userInfo: nil) + } + let cached = try #require(storage.cgPath) + #expect(cached.boundingBoxOfPath == rect) + storage.append(element: .moveToPoint, points: [200, 200], userInfo: nil) + storage.append(element: .addLineToPoint, points: [300, 300], userInfo: nil) + #expect(storage.cgPath?.boundingBoxOfPath == CGRect(x: 0, y: 0, width: 300, height: 300)) + #expect(cached.boundingBoxOfPath == rect) + } + } +} +#endif diff --git a/Tests/OpenRenderBoxCompatibilityTests/PathTestSupport.swift b/Tests/OpenRenderBoxCompatibilityTests/PathTestSupport.swift new file mode 100644 index 0000000..6d0640e --- /dev/null +++ b/Tests/OpenRenderBoxCompatibilityTests/PathTestSupport.swift @@ -0,0 +1,160 @@ +// +// PathTestSupport.swift +// OpenRenderBoxCompatibilityTests + +#if canImport(CoreGraphics) && (OPENRENDERBOX_CF_CGTYPES || OPENRENDERBOX_COMPATIBILITY_TEST) +import CoreGraphics +import Foundation + +// The curve reference data comes from RenderBox in iOS 18.5. +let referencePathGeometryEnabled: Bool = { + #if OPENRENDERBOX_COMPATIBILITY_TEST + #if os(iOS) + let version = ProcessInfo.processInfo.operatingSystemVersion + return version.majorVersion == 18 && version.minorVersion == 5 + #else + return false + #endif + #else + return true + #endif +}() + +func withPathStorage( + capacity: Int = 96, + source: ORBPath.Storage? = nil, + _ body: (ORBPath.Storage) throws -> Result +) rethrows -> Result { + let bytes = UnsafeMutableRawPointer.allocate(byteCount: capacity, alignment: 16) + let storage = unsafeBitCast(bytes, to: ORBPath.Storage.self) + storage.initialize(capacity: capacity, source: source) + defer { + storage.destroy() + bytes.deallocate() + } + return try body(storage) +} + +func withStoragePath(_ storage: ORBPath.Storage, _ body: (ORBPath) throws -> Result) rethrows -> Result { + var callbacks = ORBPath.Callbacks() + callbacks.apply = { object, info, callback in + let storage = unsafeBitCast(object, to: ORBPath.Storage.self) + return storage.apply(info: info, callback: callback) + } + callbacks.isEmpty = { object in + unsafeBitCast(object, to: ORBPath.Storage.self).isEmpty + } + callbacks.isSingleElement = { object in + unsafeBitCast(object, to: ORBPath.Storage.self).isSingleElement + } + return try withUnsafePointer(to: &callbacks) { callbacks in + try body(ORBPath(storage: storage, callbacks: callbacks)) + } +} + +struct PathElementRecord: Equatable { + var element: Int32 + var points: [CGFloat] +} + +func recordPathElement( + info: UnsafeMutableRawPointer, + element: ORBPath.Element, + points: UnsafePointer, + userInfo: UnsafeRawPointer? +) -> Bool { + let count: Int + switch element { + case .moveToPoint, .addLineToPoint: count = 2 + case .addQuadCurveToPoint, .rect, .roundedRect, .ellipse: count = 4 + case .addCurveToPoint, .fixedRoundedRectCircular, .fixedRoundedRectContinuous: count = 6 + case .unevenRoundedRectCircular, .unevenRoundedRectContinuous: count = 8 + default: count = 0 + } + info.assumingMemoryBound(to: [PathElementRecord].self).pointee.append( + PathElementRecord(element: element.rawValue, points: Array(UnsafeBufferPointer(start: points, count: count))) + ) + return true +} + +func pathElements(_ storage: ORBPath.Storage) -> [PathElementRecord] { + var elements: [PathElementRecord] = [] + _ = withUnsafeMutablePointer(to: &elements) { + storage.apply(info: $0, callback: recordPathElement) + } + return elements +} + +func pathElements(_ path: ORBPath) -> [PathElementRecord] { + var elements: [PathElementRecord] = [] + _ = withUnsafeMutablePointer(to: &elements) { + path.apply(info: $0, callback: recordPathElement) + } + return elements +} +#endif + +#if canImport(CoreGraphics) && (OPENRENDERBOX_CF_CGTYPES || OPENRENDERBOX_COMPATIBILITY_TEST) +func cgPathElements(_ path: CGPath) -> [PathElementRecord] { + var elements: [PathElementRecord] = [] + path.applyWithBlock { element in + let element = element.pointee + let count: Int + switch element.type { + case .moveToPoint, .addLineToPoint: count = 1 + case .addQuadCurveToPoint: count = 2 + case .addCurveToPoint: count = 3 + case .closeSubpath: count = 0 + @unknown default: count = 0 + } + let values = UnsafeBufferPointer(start: element.points, count: count).flatMap { [$0.x, $0.y] } + elements.append(PathElementRecord(element: element.type.rawValue, points: values)) + } + return elements +} + +// Ignore commands that do not change the geometry of a closed clip result. +func normalizedClipElements(_ elements: [PathElementRecord]) -> [PathElementRecord] { + func near(_ lhs: CGPoint, _ rhs: CGPoint) -> Bool { + abs(lhs.x - rhs.x) < 1e-8 && abs(lhs.y - rhs.y) < 1e-8 + } + func endpoint(_ element: PathElementRecord) -> CGPoint { + CGPoint(x: element.points[element.points.count - 2], y: element.points[element.points.count - 1]) + } + var result: [PathElementRecord] = [] + var current = CGPoint.zero + var start = CGPoint.zero + for element in elements { + if element.element == 0 { + start = endpoint(element) + } else if element.element == 1 { + let next = endpoint(element) + if near(current, next) { continue } + if result.count >= 2, result.last?.element == 1, + !result[result.count - 2].points.isEmpty { + let previous = endpoint(result[result.count - 2]) + let a = CGPoint(x: current.x - previous.x, y: current.y - previous.y) + let b = CGPoint(x: next.x - current.x, y: next.y - current.y) + if abs(a.x * b.y - a.y * b.x) < 1e-8 && a.x * b.x + a.y * b.y >= 0 { + result.removeLast() + } + } + } else if element.element == 2 || element.element == 3 { + let isDegenerate = stride(from: 0, to: element.points.count, by: 2).allSatisfy { + near(current, CGPoint(x: element.points[$0], y: element.points[$0 + 1])) + } + if isDegenerate { continue } + } else if element.element == 4 { + if result.last?.element == 1, near(current, start) { + result.removeLast() + } + current = start + result.append(element) + continue + } + current = endpoint(element) + result.append(element) + } + return result +} +#endif diff --git a/Tests/OpenRenderBoxCompatibilityTests/PathTests.swift b/Tests/OpenRenderBoxCompatibilityTests/PathTests.swift index 2718190..e526a94 100644 --- a/Tests/OpenRenderBoxCompatibilityTests/PathTests.swift +++ b/Tests/OpenRenderBoxCompatibilityTests/PathTests.swift @@ -4,10 +4,10 @@ import Testing -#if canImport(CoreGraphics) +#if canImport(CoreGraphics) && (OPENRENDERBOX_CF_CGTYPES || OPENRENDERBOX_COMPATIBILITY_TEST) import CoreGraphics -@Suite(.enabled(if: compatibilityTestEnabled)) +@Suite struct PathTests { @Test func createRectPath() { @@ -33,7 +33,7 @@ struct PathTests { @Test func createUnevenRoundedRectPath() { let rect = CGRect(x: 0, y: 0, width: 100, height: 100) - let path = ORBPath(roundedRect: rect, topLeftRadius: 5, bottomLeftRadius: 10, bottomRightRadius: 15, topRightRadius: 20, style: .continuous, transform: nil) + let path = ORBPath(roundedRect: rect, topLeftRadius: 5, topRightRadius: 10, bottomRightRadius: 15, bottomLeftRadius: 20, style: .continuous, transform: nil) path.release() } @@ -41,8 +41,8 @@ struct PathTests { func pathRetainRelease() { let rect = CGRect(x: 0, y: 0, width: 100, height: 100) let path = ORBPath(rect: rect, transform: nil) - path.retain() - path.release() + let retained = path.retain() + retained.release() path.release() } @@ -50,6 +50,12 @@ struct PathTests { (CGPoint(x: 50, y: 50), true), (CGPoint(x: 150, y: 150), false), (CGPoint(x: 0, y: 0), true), + (CGPoint(x: 50, y: 0), true), + (CGPoint(x: 100, y: 0), false), + (CGPoint(x: 100, y: 50), false), + (CGPoint(x: 50, y: 100), false), + (CGPoint(x: 0, y: 100), false), + (CGPoint(x: 0, y: 50), true), (CGPoint(x: 100, y: 100), false), (CGPoint(x: -1, y: 50), false), ]) @@ -82,7 +88,7 @@ struct PathTests { countPtr.pointee += 1 return true // continue iteration } - #expect(elementCount == 5) // move + 4 lines for rect + #expect(elementCount == 5) // Move, three lines, and close. path.release() } diff --git a/Tests/OpenRenderBoxCompatibilityTests/PathWrapperTests.swift b/Tests/OpenRenderBoxCompatibilityTests/PathWrapperTests.swift new file mode 100644 index 0000000..e5245c3 --- /dev/null +++ b/Tests/OpenRenderBoxCompatibilityTests/PathWrapperTests.swift @@ -0,0 +1,182 @@ +// +// PathWrapperTests.swift +// OpenRenderBoxCompatibilityTests + +import Testing + +#if canImport(CoreGraphics) && (OPENRENDERBOX_CF_CGTYPES || OPENRENDERBOX_COMPATIBILITY_TEST) +import CoreGraphics + +@Suite +struct PathWrapperTests { + private func makeLine() -> ORBPath { + let cgPath = CGMutablePath() + cgPath.move(to: .zero) + cgPath.addLine(to: CGPoint(x: 10, y: 0)) + return ORBPath(cgPath: cgPath) + } + + @Test + func transformedPathRetainsSource() { + let source = makeLine() + var transform = CGAffineTransform(a: 2, b: 0, c: 0, d: 3, tx: 20, ty: 30) + let path = source.copy(transform: &transform) + transform = .identity + source.release() + defer { path.release() } + #expect(path.isSingleElement) + #expect(pathElements(path).map(\.element) == [17, 0, 1, 16]) + #expect(cgPathElements(path.cgPath) == [ + PathElementRecord(element: 0, points: [20, 30]), + PathElementRecord(element: 1, points: [40, 30]), + ]) + } + + @Test + func retainedNestedShapeSurvivesWrapperRelease() { + let source = makeLine() + var transform = CGAffineTransform(translationX: 20, y: 30) + let path = source.copy(transform: &transform) + source.release() + var nested = ORBPath.empty + var type = ORBPathShapeType.rect + #expect(path.applyShape(allowLine: true, info: &type) { info, type, _ in + info.assumingMemoryBound(to: ORBPathShapeType.self).pointee = type + }) + #expect(type == .nested) + #expect(path.applyShape(allowLine: true, info: &nested) { info, type, shape in + if type == .nested { + info.assumingMemoryBound(to: ORBPath.self).pointee = shape.pointee.nested.path.retain() + } + }) + path.release() + defer { nested.release() } + #expect(cgPathElements(nested.cgPath) == [ + PathElementRecord(element: 0, points: [0, 0]), + PathElementRecord(element: 1, points: [10, 0]), + ]) + } + + @Test + func nestedWrappersKeepChildDepth() { + let source = makeLine() + let trimmed = source.trimmed(from: 0.25, to: 0.75) + source.release() + var transform = CGAffineTransform(translationX: 20, y: 30) + let path = trimmed.copy(transform: &transform) + trimmed.release() + #expect(path.isSingleElement) + #expect(pathElements(path).map(\.element) == [17, 18, 0, 1, 16, 16]) + #expect(cgPathElements(path.cgPath) == [ + PathElementRecord(element: 0, points: [22.5, 30]), + PathElementRecord(element: 1, points: [27.5, 30]), + ]) + var child = ORBPath.empty + #expect(path.applyShape(allowLine: true, info: &child) { info, type, shape in + if type == .nested { + info.assumingMemoryBound(to: ORBPath.self).pointee = shape.pointee.nested.path.retain() + } + }) + path.release() + defer { child.release() } + var shapeType = ORBPathShapeType.rect + #expect(child.applyShape(allowLine: true, info: &shapeType) { info, type, _ in + info.assumingMemoryBound(to: ORBPathShapeType.self).pointee = type + }) + #expect(shapeType == .trimmed) + #expect(cgPathElements(child.cgPath) == [ + PathElementRecord(element: 0, points: [2.5, 0]), + PathElementRecord(element: 1, points: [7.5, 0]), + ]) + } + + @Test + func trimmedPathPreservesSegmentBoundaries() { + let cgPath = CGMutablePath() + cgPath.move(to: .zero) + cgPath.addLine(to: CGPoint(x: 10, y: 0)) + cgPath.addLine(to: CGPoint(x: 10, y: 10)) + let source = ORBPath(cgPath: cgPath) + let path = source.trimmed(from: 0.25, to: 0.75) + source.release() + defer { path.release() } + #expect(path.isSingleElement) + #expect(pathElements(path).map(\.element) == [18, 0, 1, 1, 16]) + #expect(cgPathElements(path.cgPath) == [ + PathElementRecord(element: 0, points: [5, 0]), + PathElementRecord(element: 1, points: [10, 0]), + PathElementRecord(element: 1, points: [10, 5]), + ]) + var shapeType = ORBPathShapeType.rect + #expect(path.applyShape(allowLine: true, info: &shapeType) { info, type, _ in + info.assumingMemoryBound(to: ORBPathShapeType.self).pointee = type + }) + #expect(shapeType == .trimmed) + } + + @Test(.enabled(if: referencePathGeometryEnabled), arguments: [false, true]) + func trimmedCurvesMatchSegmentGeometry(quadratic: Bool) { + let cgPath = CGMutablePath() + cgPath.move(to: .zero) + if quadratic { + cgPath.addQuadCurve(to: CGPoint(x: 10, y: 0), control: CGPoint(x: 5, y: 10)) + } else { + cgPath.addCurve(to: CGPoint(x: 10, y: 0), control1: CGPoint(x: 0, y: 10), control2: CGPoint(x: 10, y: 10)) + } + let source = ORBPath(cgPath: cgPath) + let path = source.trimmed(from: 0.25, to: 0.75) + source.release() + defer { path.release() } + let elements = cgPathElements(path.cgPath) + #expect(elements.map(\.element) == [0, 3]) + let actual = elements.flatMap(\.points) + let expected: [CGFloat] = quadratic + ? [1.9837460658183548, 3.1804424409067495, + 3.9947165820760118, 5.6066815333809306, + 6.0056870983336692, 5.6065191755005594, + 8.0166576145913258, 3.1799553672656353] + : [1.0906342200107548, 4.8932050683032262, + 3.0138364775468443, 8.3689316438989252, + 6.9861635224531566, 8.3689316438989252, + 8.9093657799892458, 4.8932050683032253] + #expect(actual.count == expected.count) + for (actual, expected) in zip(actual, expected) { + #expect(abs(actual - expected) < 1e-10) + } + } + + @Test(arguments: [(0.75, 0.25), (0.0, 0.0), (1.0, 1.0)]) + func emptyTrimProducesNoGeometry(from: Double, to: Double) { + let source = makeLine() + let path = source.trimmed(from: from, to: to) + source.release() + defer { path.release() } + #expect(path.cgPath.isEmpty) + } + + @Test(arguments: [Int32(0), 1, 2]) + func strokedPathUsesLineCap(cap: Int32) { + let source = makeLine() + let path = source.stroked(lineWidth: 4, lineCap: cap, lineJoin: 0, miterLimit: 10, dashPhase: 0, dash: nil, dashCount: 0) + source.release() + defer { path.release() } + #expect(path.isSingleElement) + #expect(pathElements(path).map(\.element) == [19, 0, 1, 16]) + let bounds = cap == 0 ? CGRect(x: 0, y: -2, width: 10, height: 4) : CGRect(x: -2, y: -2, width: 14, height: 4) + #expect(path.cgPath.boundingBoxOfPath == bounds) + var shapeType = ORBPathShapeType.rect + #expect(path.applyShape(allowLine: true, info: &shapeType) { info, type, _ in + info.assumingMemoryBound(to: ORBPathShapeType.self).pointee = type + }) + #expect(shapeType == .stroked) + var hasEmptyDashBuffer = false + #expect(path.applyShape(allowLine: true, info: &hasEmptyDashBuffer) { info, type, shape in + if type == .stroked { + let style = shape.pointee.stroked.style + info.assumingMemoryBound(to: Bool.self).pointee = style.dashCount == 0 && style.dash != nil + } + }) + #expect(hasEmptyDashBuffer) + } +} +#endif diff --git a/Tests/OpenRenderBoxCompatibilityTests/README.md b/Tests/OpenRenderBoxCompatibilityTests/README.md index 78e259c..3aad5fa 100644 --- a/Tests/OpenRenderBoxCompatibilityTests/README.md +++ b/Tests/OpenRenderBoxCompatibilityTests/README.md @@ -1,3 +1,46 @@ ## OpenRenderBoxCompatibilityTests -Test public API of OpenRenderBox and run it against with RenderBox on Apple Platform. +Run the Path suites against native OpenRenderBox: + +```sh +OPENRENDERBOX_USE_LOCAL_DEPS=1 OPENRENDERBOX_RENDERBOX=0 \ +OPENRENDERBOX_LIBRARY_EVOLUTION=0 swift test --filter Path +``` + +Run the same public API suites against the host RenderBox: + +```sh +OPENRENDERBOX_USE_LOCAL_DEPS=1 OPENRENDERBOX_RENDERBOX=1 \ +OPENRENDERBOX_COMPATIBILITY_TEST=1 OPENRENDERBOX_LIBRARY_EVOLUTION=0 \ +swift test --filter 'OpenRenderBoxCompatibilityTests.*Path' +``` + +The local dependency options require sibling OpenCoreGraphics and +DarwinPrivateFrameworks checkouts. Native Path suites run without enabling +other unfinished compatibility suites. + +To test Swift Path with OpenCoreGraphics, use: + +```sh +OPENSWIFTUI_COREGRAPHICS=0 OPENRENDERBOX_CF_CGTYPES=0 \ +OPENRENDERBOX_USE_LOCAL_DEPS=1 OPENRENDERBOX_RENDERBOX=0 \ +OPENRENDERBOX_LIBRARY_EVOLUTION=0 swift test --filter PortablePathTests +``` + +Set `OPENSWIFTUI_COREGRAPHICS=1` to test Swift Path with system CoreGraphics. +This shared setting selects the same backend in ORB and OpenCoreGraphicsShims. +`OPENRENDERBOX_CF_CGTYPES` selects C++ Path (`1`) or Swift Path (`0`). + +The exact curve-trimming, line-flattening, and clip-stroke undershoot fixtures +use RenderBox from iOS 18.5 (22F77). +They run against native OpenRenderBox and Apple RenderBox on iOS 18.5. +Other Apple releases skip those fixtures because their curve subdivision and +clip-stroke behavior can differ. The suites also cover serialized data, +callback cancellation, interpolated storage lifetime, and wrapper geometry. +To test the reference runtime, open `Package.swift` in Xcode with the +Apple compatibility environment above, select the +`OpenRenderBoxCompatibilityTests` package scheme, and run the Path suites on +an iOS 18.5 Simulator. + +Storage fixtures allocate separate aligned buffers. A primitive's `storage` +pointer is not a buffer that can be passed to Storage initialization APIs. diff --git a/Tests/OpenRenderBoxTests/Path/PathTests.swift b/Tests/OpenRenderBoxTests/Path/PathTests.swift index e0b5f95..4f3cb29 100644 --- a/Tests/OpenRenderBoxTests/Path/PathTests.swift +++ b/Tests/OpenRenderBoxTests/Path/PathTests.swift @@ -5,15 +5,224 @@ // Created by Kyle on 12/21/25. // -#if canImport(Darwin) +#if canImport(Darwin) && OPENRENDERBOX_CF_CGTYPES +import Foundation +import OpenRenderBox import OpenRenderBoxCxx.Path import Testing @Suite struct PathTests { + private struct ElementCapture { + var element: ORBPath.Element? + var hasTransform = false + } + + private struct ShapeCapture { + var type: ORBPathShapeType? + var transform: CGAffineTransform = .identity + var rect: CGRect = .zero + var cornerSize: CGSize = .zero + var style: ORBPath.RoundedCornerStyle = .circular + } + + @Test + func replacesStorageCapacityBits() { + let flags = ORB.Path.StorageFlags().withCapacity(15).withCapacity(16) + #expect(flags.capacity() == 16) + } + + @Test + func appliesRectangleShape() { + let rect = CGRect(x: 10, y: 20, width: 80, height: 40) + var transform = CGAffineTransform(translationX: 5, y: 7) + let path = ORBPath(rect: rect, transform: &transform) + defer { path.release() } + + var capture = ShapeCapture() + let recognized = path.applyShape(allowLine: true, info: &capture) { info, type, shape in + let capture = info.assumingMemoryBound(to: ShapeCapture.self) + capture.pointee.type = type + capture.pointee.transform = shape.pointee.rect.transform + capture.pointee.rect = shape.pointee.rect.rect + } + + #expect(recognized) + #expect(capture.type == .rect) + #expect(capture.transform == transform) + #expect(capture.rect == rect) + #expect(path.isSingleElement) + } + @Test - func demo() { - _ = ORB.Path.Storage(96) + func appliesEllipseShape() { + let rect = CGRect(x: 1, y: 2, width: 30, height: 20) + let path = ORBPath(ellipseIn: rect, transform: nil) + defer { path.release() } + + var capture = ShapeCapture() + let recognized = path.applyShape(allowLine: true, info: &capture) { info, type, shape in + let capture = info.assumingMemoryBound(to: ShapeCapture.self) + capture.pointee.type = type + capture.pointee.transform = shape.pointee.ellipse.transform + capture.pointee.rect = shape.pointee.ellipse.rect + } + + #expect(recognized) + #expect(capture.type == .ellipse) + #expect(capture.transform.isIdentity) + #expect(capture.rect == rect) + } + + @Test + func appliesFixedRoundedRectangleShape() { + let rect = CGRect(x: 3, y: 4, width: 50, height: 30) + let path = ORBPath( + roundedRect: rect, + cornerWidth: 8, + cornerHeight: 6, + style: .continuous, + transform: nil + ) + defer { path.release() } + + var capture = ShapeCapture() + let recognized = path.applyShape(allowLine: true, info: &capture) { info, type, shape in + let capture = info.assumingMemoryBound(to: ShapeCapture.self) + capture.pointee.type = type + capture.pointee.transform = shape.pointee.fixedRoundedRect.transform + capture.pointee.rect = shape.pointee.fixedRoundedRect.rect + capture.pointee.cornerSize = shape.pointee.fixedRoundedRect.cornerSize + capture.pointee.style = shape.pointee.fixedRoundedRect.style + } + + #expect(recognized) + #expect(capture.type == .fixedRoundedRect) + #expect(capture.transform.isIdentity) + #expect(capture.rect == rect) + #expect(capture.cornerSize == CGSize(width: 8, height: 6)) + #expect(capture.style == .continuous) + } + + @Test + func primitiveCallbacksMatchSingleElementLayout() { + let rect = CGRect(x: 2, y: 3, width: 40, height: 20) + var identity = CGAffineTransform.identity + let path = ORBPath(rect: rect, transform: &identity) + defer { path.release() } + + #expect(path.callbacks.pointee.boundingRect == nil) + #expect(path.callbacks.pointee.cgPath == nil) + + var capture = ElementCapture() + let applied = path.apply(info: &capture) { info, element, _, userInfo in + let capture = info.assumingMemoryBound(to: ElementCapture.self) + capture.pointee.element = element + capture.pointee.hasTransform = userInfo != nil + return true + } + #expect(applied) + #expect(capture.element == .rect) + #expect(!capture.hasTransform) + } + + @Test + func identityAndNilTransformsProduceEqualPrimitives() { + let rect = CGRect(x: 2, y: 3, width: 40, height: 20) + var identity = CGAffineTransform.identity + let lhs = ORBPath(rect: rect, transform: nil) + let rhs = ORBPath(rect: rect, transform: &identity) + defer { + lhs.release() + rhs.release() + } + + #expect(lhs.isEqual(to: rhs)) + } + + @Test + func appliesCoreGraphicsRectangleShape() { + let rect = CGRect(x: 10, y: 20, width: 80, height: 40) + var transform = CGAffineTransform(translationX: 5, y: 7) + let cgPath = CGPath(rect: rect, transform: &transform) + let path = ORBPath(cgPath: cgPath) + defer { path.release() } + + var capture = ShapeCapture() + #expect(path.applyShape(allowLine: true, info: &capture) { info, type, shape in + let capture = info.assumingMemoryBound(to: ShapeCapture.self) + capture.pointee.type = type + capture.pointee.transform = shape.pointee.rect.transform + capture.pointee.rect = shape.pointee.rect.rect + }) + #expect(capture.type == .rect) + #expect(capture.rect.applying(capture.transform) == cgPath.boundingBoxOfPath) + } + + @Test + func zeroCornerRoundedRectangleUsesRoundedRectElement() { + let rect = CGRect(x: 3, y: 4, width: 50, height: 30) + let path = ORBPath( + roundedRect: rect, + cornerWidth: 0, + cornerHeight: 0, + style: .continuous, + transform: nil + ) + defer { path.release() } + + var capture = ElementCapture() + #expect(path.apply(info: &capture) { info, element, _, _ in + info.assumingMemoryBound(to: ElementCapture.self).pointee.element = element + return true + }) + #expect(capture.element == .roundedRect) + + var shape = ShapeCapture() + #expect(path.applyShape(allowLine: true, info: &shape) { info, type, _ in + info.assumingMemoryBound(to: ShapeCapture.self).pointee.type = type + }) + #expect(shape.type == .rect) + } + + @Test + func continuousRoundedRectangleUsesContinuousGeometry() { + let rect = CGRect(x: 0, y: 0, width: 80, height: 50) + let circular = ORBPath( + roundedRect: rect, + cornerWidth: 12, + cornerHeight: 10, + style: .circular, + transform: nil + ) + let continuous = ORBPath( + roundedRect: rect, + cornerWidth: 12, + cornerHeight: 10, + style: .continuous, + transform: nil + ) + defer { + circular.release() + continuous.release() + } + + #expect(circular.cgPath != continuous.cgPath) + } + + @Test + func primitiveBoundingRectUsesPathAPI() { + let rect = CGRect(x: 1, y: 2, width: 30, height: 20) + var transform = CGAffineTransform(rotationAngle: .pi / 4) + let path = ORBPath(ellipseIn: rect, transform: &transform) + defer { path.release() } + + let expected = rect.applying(transform) + let actual = path.boundingRect + #expect(abs(actual.origin.x - expected.origin.x) < 0.0001) + #expect(abs(actual.origin.y - expected.origin.y) < 0.0001) + #expect(abs(actual.width - expected.width) < 0.0001) + #expect(abs(actual.height - expected.height) < 0.0001) } } #endif diff --git a/Tests/OpenRenderBoxTests/Path/PortablePathTests.swift b/Tests/OpenRenderBoxTests/Path/PortablePathTests.swift new file mode 100644 index 0000000..9339939 --- /dev/null +++ b/Tests/OpenRenderBoxTests/Path/PortablePathTests.swift @@ -0,0 +1,252 @@ +// +// PortablePathTests.swift +// OpenRenderBoxTests + +#if !OPENRENDERBOX_CF_CGTYPES +import OpenCoreGraphicsShims +import OpenRenderBox +import Testing + +@Suite +struct PortablePathTests { + private func withStorage(capacity: Int = 96, source: ORBPath.Storage? = nil, _ body: (ORBPath.Storage) -> Result) -> Result { + let bytes = UnsafeMutableRawPointer.allocate(byteCount: capacity, alignment: 16) + let storage = ORBPath.Storage(OpaquePointer(bytes)) + storage.initialize(capacity: capacity, source: source) + defer { + storage.destroy() + bytes.deallocate() + } + return body(storage) + } + + private func elementCount(_ storage: ORBPath.Storage) -> Int { + var count = 0 + _ = storage.apply(info: &count) { info, _, _, _ in + info.assumingMemoryBound(to: Int.self).pointee += 1 + return true + } + return count + } + + @Test + func primitiveQueries() { + let rect = CGRect(x: 1, y: 2, width: 100, height: 80) + let path = ORBPath(rect: rect, transform: nil) + defer { path.release() } + #expect(path.boundingRect == rect) + #expect(!path.isEmpty) + #expect(path.isSingleElement) + #expect(path.contains(point: CGPoint(x: 50, y: 40), eoFill: false)) + #expect(!path.contains(point: CGPoint(x: -1, y: 40), eoFill: false)) + var type = ORBPathShapeType.line + #expect(path.applyShape(allowLine: true, info: &type) { info, type, _ in + info.assumingMemoryBound(to: ORBPathShapeType.self).pointee = type + }) + #expect(type == .rect) + } + + @Test + func retainedPathUsesReturnedStorage() { + var destination: UInt64 = 0 + withUnsafePointer(to: &destination) { destination in + var source = UnsafeRawPointer(destination) + var callbacks = ORBPath.Callbacks() + callbacks.retain = { object in + object.assumingMemoryBound(to: UnsafeRawPointer.self).pointee + } + withUnsafePointer(to: &source) { source in + withUnsafePointer(to: &callbacks) { callbacks in + let path = ORBPath(storage: ORBPath.Storage(OpaquePointer(source)), callbacks: callbacks) + let retained = path.retain() + #expect(retained.storage.rawValue == OpaquePointer(destination)) + } + } + } + } + + @Test(arguments: [ + (CGPoint(x: 5, y: 0), false), + (CGPoint(x: 10, y: 5), false), + (CGPoint(x: 5, y: 10), false), + (CGPoint(x: 0, y: 5), true), + (CGPoint(x: 5, y: 5), true), + ]) + func ellipseBoundaryContainment(point: CGPoint, expected: Bool) { + let path = ORBPath(ellipseIn: CGRect(x: 0, y: 0, width: 10, height: 10), transform: nil) + defer { path.release() } + #expect(path.contains(point: point, eoFill: false) == expected) + } + + @Test + func containmentPreservesMaskBits() { + let path = ORBPath(rect: CGRect(x: 0, y: 0, width: 10, height: 10), transform: nil) + defer { path.release() } + var points = Array(repeating: CGPoint(x: 20, y: 20), count: 65) + points[0] = CGPoint(x: 5, y: 5) + points[63] = CGPoint(x: 5, y: 5) + points[64] = CGPoint(x: 5, y: 5) + #expect(path.containsPoints(count: 65, points: points, eoFill: false, transform: nil) == (UInt64(1) | (UInt64(1) << 63))) + } + + @Test(arguments: [64, 256]) + func storageCopiesAndClearsIndependently(capacity: Int) { + withStorage(capacity: capacity) { source in + source.append(element: .moveToPoint, points: [0, 0], userInfo: nil) + for index in 1...40 { + source.append(element: .addLineToPoint, points: [CGFloat(index), CGFloat(index * 2)], userInfo: nil) + } + #expect(elementCount(source) == 41) + withStorage(capacity: 64, source: source) { copy in + #expect(copy.isEqual(to: source)) + source.clear() + #expect(source.isEmpty) + #expect(elementCount(copy) == 41) + #expect(copy.boundingRect == CGRect(x: 0, y: 0, width: 40, height: 80)) + } + } + } + + @Test + func storageUsesBezierOrderAndStopsEnumeration() { + withStorage { storage in + storage.append(element: .moveToPoint, points: [0, 0], userInfo: nil) + storage.append(element: .addQuadCurveToPoint, points: [1, 2, 3, 4], userInfo: nil) + #expect(storage.bezierOrder == 2) + storage.append(element: .addCurveToPoint, points: [5, 6, 7, 8, 9, 10], userInfo: nil) + #expect(storage.bezierOrder == 3) + var count = 0 + #expect(!storage.apply(info: &count) { info, _, _, _ in + info.assumingMemoryBound(to: Int.self).pointee += 1 + return false + }) + #expect(count == 1) + } + } + + @Test + func roundedStylesProduceDifferentElements() { + let rect = CGRect(x: 0, y: 0, width: 100, height: 80) + for style in [ORBPath.RoundedCornerStyle.circular, .continuous] { + let path = ORBPath(roundedRect: rect, topLeftRadius: 5, topRightRadius: 10, bottomRightRadius: 15, bottomLeftRadius: 20, style: style, transform: nil) + defer { path.release() } + var element = ORBPath.Element.invalid + #expect(path.apply(info: &element) { info, element, _, _ in + info.assumingMemoryBound(to: ORBPath.Element.self).pointee = element + return true + }) + #expect(element == (style == .circular ? .unevenRoundedRectCircular : .unevenRoundedRectContinuous)) + #expect(path.contains(point: CGPoint(x: 50, y: 40), eoFill: false)) + var actualStyle = ORBPath.RoundedCornerStyle.circular + #expect(path.applyShape(allowLine: true, info: &actualStyle) { info, type, shape in + if type == .unevenRoundedRect { + info.assumingMemoryBound(to: ORBPath.RoundedCornerStyle.self).pointee = shape.pointee.unevenRoundedRect.style + } + }) + #expect(actualStyle == style) + } + } + + #if !OPENRENDERBOX_COREGRAPHICS + @Test + func trimmedCubicGeometry() { + let cgPath = CGMutablePath() + cgPath.move(to: .zero) + cgPath.addCurve(to: CGPoint(x: 10, y: 0), control1: CGPoint(x: 0, y: 10), control2: CGPoint(x: 10, y: 10)) + let source = ORBPath(cgPath: cgPath) + let path = source.trimmed(from: 0.25, to: 0.75) + source.release() + defer { path.release() } + let elements = path.cgPath.elements + #expect(elements.count == 2) + var actual: [CGFloat] = [] + for element in elements { + switch element { + case let .moveToPoint(point): actual += [point.x, point.y] + case let .addCurveToPoint(first, second, end): actual += [first.x, first.y, second.x, second.y, end.x, end.y] + default: Issue.record("Unexpected trimmed curve element") + } + } + let expected: [CGFloat] = [1.0906342200107548, 4.8932050683032262, + 3.0138364775468443, 8.3689316438989252, + 6.9861635224531566, 8.3689316438989252, + 8.9093657799892458, 4.8932050683032253] + #expect(actual.count == expected.count) + for (actual, expected) in zip(actual, expected) { + #expect(abs(actual - expected) < 1e-10) + } + } + + @Test + func relativeArcGeometry() { + withStorage { storage in + storage.append(element: .relativeArc, points: [0, 0, 10, 0, .pi / 2], userInfo: nil) + guard let path = storage.cgPath else { + Issue.record("Missing arc path") + return + } + #expect(path.elements.count == 2) + #expect(path.elements.first == .moveToPoint(CGPoint(x: 10, y: 0))) + if case let .addCurveToPoint(_, _, end) = path.elements.last { + #expect(abs(end.x) < 1e-6) + #expect(abs(end.y - 10) < 1e-6) + } else { + Issue.record("Missing arc curve") + } + } + } + + @Test(arguments: [ORBPath.RoundedCornerStyle.circular, .continuous]) + func roundedRectangleClampsBothAxes(style: ORBPath.RoundedCornerStyle) { + let rect = CGRect(x: 0, y: 0, width: 100, height: 20) + let oversized = ORBPath(roundedRect: rect, cornerWidth: 50, cornerHeight: 50, style: style, transform: nil) + let clamped = ORBPath(roundedRect: rect, cornerWidth: 10, cornerHeight: 10, style: style, transform: nil) + let singleAxis = ORBPath(roundedRect: rect, cornerWidth: 0, cornerHeight: 10, style: style, transform: nil) + let square = ORBPath(roundedRect: rect, cornerWidth: 0, cornerHeight: 0, style: style, transform: nil) + defer { + oversized.release() + clamped.release() + singleAxis.release() + square.release() + } + #expect(oversized.cgPath == clamped.cgPath) + #expect(singleAxis.cgPath.elements.count == (style == .circular ? 10 : 18)) + #expect(square.cgPath.elements.count == 6) + } + + @Test + func continuousRoundedRectangleGeometry() { + let path = ORBPath(roundedRect: CGRect(x: 0, y: 0, width: 100, height: 80), cornerWidth: 10, cornerHeight: 20, style: .continuous, transform: nil) + defer { path.release() } + let elements = path.cgPath.elements + #expect(elements.count == 18) + #expect(elements[0] == .moveToPoint(CGPoint(x: 100, y: 40))) + #expect(elements[1] == .addLineToPoint(CGPoint(x: 100, y: 49.426701068878174))) + #expect(elements[2] == .addCurveToPoint( + CGPoint(x: 100, y: 58.230199813842773), + CGPoint(x: 100, y: 62.63185977935791), + CGPoint(x: 99.250885993242264, y: 67.370120286941528) + )) + } + #endif + + @Test + func nestedShapeSurvivesSourceRelease() { + let source = ORBPath(rect: CGRect(x: 1, y: 2, width: 10, height: 20), transform: nil) + var transform = CGAffineTransform(translationX: 20, y: 30) + let path = source.copy(transform: &transform) + source.release() + var nested = ORBPath.empty + #expect(path.isSingleElement) + #expect(path.boundingRect == CGRect(x: 21, y: 32, width: 10, height: 20)) + #expect(path.applyShape(allowLine: true, info: &nested) { info, type, shape in + if type == .nested { + info.assumingMemoryBound(to: ORBPath.self).pointee = shape.pointee.nested.path.retain() + } + }) + path.release() + defer { nested.release() } + #expect(nested.boundingRect == CGRect(x: 1, y: 2, width: 10, height: 20)) + } +} +#endif