├── .gitignore ├── Images ├── debug_mesh.png └── debug_points.png ├── VisualDebugger.xcworkspace ├── Playground.playground │ ├── Pages │ │ ├── TestMixDebugging.xcplaygroundpage │ │ │ ├── timeline.xctimeline │ │ │ └── Contents.swift │ │ ├── TestMorePoints.xcplaygroundpage │ │ │ ├── timeline.xctimeline │ │ │ └── Contents.swift │ │ ├── TestImplementation.xcplaygroundpage │ │ │ ├── timeline.xctimeline │ │ │ └── Contents.swift │ │ ├── TestDebugMatrix.xcplaygroundpage │ │ │ └── Contents.swift │ │ ├── TestRects.xcplaygroundpage │ │ │ └── Contents.swift │ │ └── TestTinyArea.xcplaygroundpage │ │ │ └── Contents.swift │ └── contents.xcplayground └── contents.xcworkspacedata ├── Sources ├── VisualUtils │ ├── CGSize+Behavior.swift │ ├── Anchor.swift │ ├── AppColor+Behavior.swift │ ├── CGContext+Behavior.swift │ ├── CGPoint+Behavior.swift │ ├── Segment.swift │ ├── Alias.swift │ ├── CGRect+Behavior.swift │ └── RectFitter.swift └── VisualDebugger │ ├── extensions │ ├── ContextRenderable.swift │ ├── CGRect+.swift │ └── AppBezierPath+.swift │ ├── render │ ├── base │ │ ├── segment │ │ │ ├── SegmentRenderer.swift │ │ │ ├── shapes │ │ │ │ ├── DoubleArrow.swift │ │ │ │ └── Arrow.swift │ │ │ └── SegmentRenderElement.swift │ │ ├── static │ │ │ ├── StaticRendable.swift │ │ │ ├── ShapeRenderer.swift │ │ │ ├── shapes │ │ │ │ ├── EmptyShape.swift │ │ │ │ ├── Circle.swift │ │ │ │ └── PolygonShape.swift │ │ │ ├── TextLocation.swift │ │ │ ├── TextSource.swift │ │ │ ├── StaticRenderElement.swift │ │ │ ├── ShapeElement.swift │ │ │ └── TextElement.swift │ │ ├── ShapeRenderElement.swift │ │ ├── TextRenderStyle+Bg.swift │ │ ├── face │ │ │ ├── FaceRenderElement.swift │ │ │ └── VectorTriangleRenderElement.swift │ │ └── ShapeRenderStyle.swift │ └── compositions │ │ └── PointElement.swift │ ├── coordinateSystem │ ├── CoordinateSystem2D.swift │ ├── CoordinateStyle.swift │ ├── Utils.swift │ ├── Coordinate.swift │ ├── UIBezierPath+Behavior.swift │ ├── AxisData.swift │ ├── Axis.swift │ ├── CoordinateRenderElement.swift │ ├── Styles.swift │ └── AxisRenderElement.swift │ ├── debuggers │ ├── Mesh+Definitions.swift │ ├── base │ │ ├── BaseDebugger.swift │ │ ├── GeometryDebugger.swift │ │ └── SegmentDebugger.swift │ ├── Mesh+Debug.swift │ ├── Mesh+Get.swift │ ├── Path.swift │ ├── Dot.swift │ ├── Logger.swift │ ├── Line.swift │ ├── Polygon.swift │ ├── Lines.swift │ ├── VectorMesh.swift │ └── Ray.swift │ ├── DebugManager.swift │ ├── Debuggable.swift │ ├── algorithms │ ├── DebugRenderBuilder.swift │ └── SegmentsCollection.swift │ ├── data │ └── VectorTriangle.swift │ ├── DebugCapture.swift │ └── DebugView.swift ├── VisualDebugger.podspec ├── Tests └── VisualDebuggerTests │ └── Test.swift ├── LICENSE ├── Package.swift ├── .github └── copilot-instructions.md ├── merge_code.command └── readme-cn.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | -------------------------------------------------------------------------------- /Images/debug_mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyunguiMilook/VisualDebugger/HEAD/Images/debug_mesh.png -------------------------------------------------------------------------------- /Images/debug_points.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyunguiMilook/VisualDebugger/HEAD/Images/debug_points.png -------------------------------------------------------------------------------- /VisualDebugger.xcworkspace/Playground.playground/Pages/TestMixDebugging.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /VisualDebugger.xcworkspace/Playground.playground/Pages/TestMorePoints.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /VisualDebugger.xcworkspace/Playground.playground/Pages/TestImplementation.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Sources/VisualUtils/CGSize+Behavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | extension CGSize { 11 | public static let unit: CGSize = CGSize(width: 1, height: 1) 12 | } 13 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/extensions/ContextRenderable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextRenderable.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import Foundation 9 | import CoreGraphics 10 | 11 | 12 | public protocol ContextRenderElementOwner { 13 | var renderElement: ContextRenderable { get } 14 | } 15 | -------------------------------------------------------------------------------- /VisualDebugger.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/segment/SegmentRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SegmentRenderer.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/3. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public protocol SegmentRenderer { 12 | func getBezierPath(start: CGPoint, end: CGPoint) -> AppBezierPath 13 | } 14 | -------------------------------------------------------------------------------- /VisualDebugger.xcworkspace/Playground.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/segment/shapes/DoubleArrow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoubleArrow.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/4. 6 | // 7 | 8 | public struct DoubleArrow { 9 | public enum Direction { 10 | case normal 11 | case reverse 12 | } 13 | 14 | public let direction: Direction 15 | public let tip: Arrow.Tip 16 | 17 | public init(direction: Direction, tip: Arrow.Tip) { 18 | self.direction = direction 19 | self.tip = tip 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/static/StaticRendable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rotateable.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/2. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public protocol Cloneable { 12 | func clone() -> Self 13 | } 14 | 15 | public protocol StaticRendable: Cloneable { 16 | // raw bounds 17 | var contentBounds: CGRect { get } 18 | 19 | func render( 20 | with transform: Matrix2D, 21 | in context: CGContext, 22 | scale: CGFloat, 23 | contextHeight: Int? 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/coordinateSystem/CoordinateSystem2D.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinateSystem2D.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | 9 | import Foundation 10 | 11 | public enum CoordinateSystem2D: Int, Sendable { 12 | public static let UIKit: CoordinateSystem2D = .yDown 13 | public static let UV: CoordinateSystem2D = .yUp 14 | public static let OpenGL: CoordinateSystem2D = .yUp 15 | 16 | case yDown, yUp 17 | } 18 | 19 | extension CoordinateSystem2D { 20 | public var flipped: Self { 21 | switch self { 22 | case .yDown: return .yUp 23 | case .yUp: return .yDown 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/extensions/CGRect+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRect+.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/7/28. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | #if canImport(UIKit) 11 | import UIKit 12 | #elseif canImport(AppKit) 13 | import AppKit 14 | #endif 15 | 16 | extension CGRect { 17 | public func getPath( 18 | name: String? = nil, 19 | color: AppColor = .yellow, 20 | style: Path.PathStyle = .stroke() 21 | ) -> Path { 22 | Path( 23 | path: AppBezierPath(rect: self), 24 | name: name, 25 | transform: .identity, 26 | color: color, 27 | style: style 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/extensions/AppBezierPath+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppBezierPath+.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/7/28. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | #if canImport(UIKit) 11 | import UIKit 12 | #elseif canImport(AppKit) 13 | import AppKit 14 | #endif 15 | 16 | extension AppBezierPath { 17 | 18 | public func getPath( 19 | name: String? = nil, 20 | color: AppColor = .yellow, 21 | style: Path.PathStyle = .stroke() 22 | ) -> Path { 23 | Path( 24 | path: self, 25 | name: name, 26 | transform: .identity, 27 | color: color, 28 | style: style 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /VisualDebugger.podspec: -------------------------------------------------------------------------------- 1 | #pod lint VisualDebugger.podspec 2 | #pod trunk push VisualDebugger.podspec 3 | 4 | Pod::Spec.new do |s| 5 | s.name = "VisualDebugger" 6 | s.version = "2.0.0" 7 | s.summary = "The most elegant and easiest way to visual you data in playground" 8 | s.homepage = "https://github.com/chenyunguiMilook/VisualDebugger" 9 | s.license = { :type => "MIT" } 10 | s.authors = { "chenyunguiMilook" => "286224043@qq.com" } 11 | 12 | s.requires_arc = true 13 | s.osx.deployment_target = "10.11" 14 | s.ios.deployment_target = "8.0" 15 | s.source = { :git => "https://github.com/chenyunguiMilook/VisualDebugger.git", :tag => s.version } 16 | s.source_files = ['Source/*.swift', 'Source/**/*.swift'] 17 | end 18 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/static/ShapeRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapeSource.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/2. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public protocol ShapeRenderer: Sendable { 12 | var radius: Double { get } 13 | var bounds: CGRect { get } 14 | func getBezierPath() -> AppBezierPath 15 | } 16 | 17 | extension AppBezierPath: @retroactive @unchecked Sendable {} 18 | extension AppBezierPath: ShapeRenderer { 19 | public var radius: Double { 20 | let w = self.bounds.width / 2 21 | let h = self.bounds.height / 2 22 | return max(w, h) 23 | } 24 | public func getBezierPath() -> AppBezierPath { 25 | self 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /VisualDebugger.xcworkspace/Playground.playground/Pages/TestDebugMatrix.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import CoreGraphics 5 | import VisualDebugger 6 | import UIKit 7 | 8 | public func * (lhs: CGAffineTransform, rhs: CGAffineTransform) -> CGAffineTransform { 9 | return lhs.concatenating(rhs) 10 | } 11 | 12 | let scale = CGAffineTransform(scaleX: 1.2, y: 1.4) 13 | let rotate = CGAffineTransform(rotationAngle: CGFloat.pi/4) 14 | let translate = CGAffineTransform(translationX: 10, y: 10) 15 | let transform = scale * rotate * translate 16 | 17 | let affineRect = CGRect(origin: .zero, size: CGSize(width: 100, height: 100)).affineRect 18 | 19 | transform.getAffineTransform(rect: affineRect).debugView 20 | 21 | 22 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/static/shapes/EmptyShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyShape.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/22. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public struct EmptyShape: ShapeRenderer { 12 | public var center: CGPoint 13 | public var radius: Double = 0 14 | 15 | public var bounds: CGRect { 16 | CGRect(center: center, size: .zero) 17 | } 18 | 19 | public init(center: CGPoint = .zero) { 20 | self.center = center 21 | } 22 | 23 | public func getBezierPath() -> AppBezierPath { 24 | AppBezierPath() 25 | } 26 | } 27 | 28 | extension ShapeRenderer where Self == EmptyShape { 29 | public static func empty() -> EmptyShape { 30 | return EmptyShape() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/VisualDebuggerTests/Test.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Test.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import Testing 9 | @testable import VisualDebugger 10 | #if canImport(UIKit) 11 | import UIKit 12 | #elseif canImport(AppKit) 13 | import AppKit 14 | #endif 15 | 16 | struct Test { 17 | 18 | @MainActor 19 | @Test func testPointDebug() { 20 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 21 | let context = DebugContext(elements: [ 22 | Polygon([ 23 | .init(x: 10, y: 10), 24 | .init(x: 10, y: 23), 25 | .init(x: 23, y: 67) 26 | ]) 27 | .setVertexStyle(at: 0, label: "A") 28 | ]) 29 | 30 | print(context) 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/Mesh+Definitions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mesh+Definitions.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/3. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | extension Mesh { 11 | public struct Face { 12 | public var v0: Int 13 | public var v1: Int 14 | public var v2: Int 15 | 16 | public init(_ v0: Int, _ v1: Int, _ v2: Int) { 17 | self.v0 = v0 18 | self.v1 = v1 19 | self.v2 = v2 20 | } 21 | } 22 | 23 | } 24 | 25 | extension Mesh.Face: Equatable { 26 | public static func ==(lhs: Self, rhs: Self) -> Bool { 27 | // 只要是使用的相同的三个顶点,就代表相等,不考虑顶点顺序 28 | let lhsVertices = Set([lhs.v0, lhs.v1, lhs.v2]) 29 | let rhsVertices = Set([rhs.v0, rhs.v1, rhs.v2]) 30 | return lhsVertices == rhsVertices 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/static/shapes/Circle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Circle.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/28. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public struct Circle: ShapeRenderer { 12 | public var center: CGPoint 13 | public var radius: Double 14 | 15 | public var bounds: CGRect { 16 | CGRect(center: center, size: CGSize(width: radius * 2, height: radius * 2)) 17 | } 18 | 19 | public init(center: CGPoint = .zero, radius: Double) { 20 | self.center = center 21 | self.radius = radius 22 | } 23 | 24 | public func getBezierPath() -> AppBezierPath { 25 | AppBezierPath(ovalIn: bounds) 26 | } 27 | } 28 | 29 | extension ShapeRenderer where Self == Circle { 30 | public static func circle(center: CGPoint = .zero, radius: Double) -> Circle { 31 | return Circle(center: center, radius: radius) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/static/TextLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextPoint.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/2. 6 | // 7 | 8 | import VisualUtils 9 | 10 | public enum TextLocation: String, Sendable { 11 | case center 12 | case left, right, top, bottom 13 | case topLeft, topRight, bottomLeft, bottomRight 14 | } 15 | 16 | extension TextLocation { 17 | var anchor: Anchor { 18 | switch self { 19 | case .center: 20 | return .midCenter 21 | case .left: 22 | return .midRight 23 | case .right: 24 | return .midLeft 25 | case .top: 26 | return .btmCenter 27 | case .bottom: 28 | return .topCenter 29 | case .topLeft: 30 | return .btmRight 31 | case .topRight: 32 | return .btmLeft 33 | case .bottomLeft: 34 | return .topRight 35 | case .bottomRight: 36 | return .topLeft 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/DebugManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Debugger.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/22. 6 | // 7 | import Foundation 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | // manager global debug elements 12 | public final class DebugManager: @unchecked Sendable { 13 | // 单例模式 14 | public static let shared: DebugManager = .init() 15 | 16 | private let queue = DispatchQueue(label: "logger.queue") 17 | public private(set) var elements: [any ContextRenderable] = [] 18 | 19 | private init() { 20 | 21 | } 22 | 23 | public func add(_ element: any ContextRenderable) { 24 | queue.sync { 25 | self.elements.append(element) 26 | } 27 | } 28 | } 29 | 30 | extension DebugManager: ContextRenderable { 31 | public func render(with transform: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 32 | for element in elements { 33 | element.render(with: transform, in: context, scale: scale, contextHeight: contextHeight) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/coordinateSystem/CoordinateStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinateStyle.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/4. 6 | // 7 | 8 | import VisualUtils 9 | 10 | public struct CoordinateStyle: @unchecked Sendable { 11 | 12 | public let xAxisColor: AppColor 13 | public let yAxisColor: AppColor 14 | public let originColor: AppColor 15 | 16 | public init(xAxisColor: AppColor, yAxisColor: AppColor, originColor: AppColor) { 17 | self.xAxisColor = xAxisColor 18 | self.yAxisColor = yAxisColor 19 | self.originColor = originColor 20 | } 21 | } 22 | 23 | extension CoordinateStyle { 24 | public static let `default`: Self = .init( 25 | xAxisColor: .lightGray.withAlphaComponent(0.5), 26 | yAxisColor: .lightGray.withAlphaComponent(0.5), 27 | originColor: .lightGray 28 | ) 29 | 30 | public static let color: Self = .init( 31 | xAxisColor: .red.withAlphaComponent(0.5), 32 | yAxisColor: .green.withAlphaComponent(0.5), 33 | originColor: .lightGray 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/ShapeRenderElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapeRenderElement.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import Foundation 9 | import CoreGraphics 10 | import VisualUtils 11 | 12 | public struct ShapeRenderElement: DebugRenderable { 13 | 14 | // TODO: 添加一个transform属性, 这样原始路径可以被保存,不需要频繁变换路径,只在渲染的时候计算即可? 15 | public let path: AppBezierPath 16 | public let transform: Matrix2D 17 | public let style: ShapeRenderStyle 18 | 19 | public init(path: AppBezierPath, transform: Matrix2D = .identity, style: ShapeRenderStyle) { 20 | self.path = path 21 | self.transform = transform 22 | self.style = style 23 | } 24 | 25 | public var debugBounds: CGRect? { 26 | self.path.bounds * self.transform 27 | } 28 | 29 | public func render(with transform: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 30 | guard let p = path * (self.transform * transform) else { return } 31 | context.render(path: p.cgPath, style: style) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/VisualUtils/Anchor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Anchor.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import CoreGraphics 9 | import Foundation 10 | 11 | public enum Anchor: String, Codable, CaseIterable, Sendable { 12 | case topLeft, topCenter, topRight 13 | case midLeft, midCenter, midRight 14 | case btmLeft, btmCenter, btmRight 15 | } 16 | 17 | extension Anchor { 18 | 19 | public var anchor: CGPoint { 20 | switch self { 21 | case .topLeft: 22 | CGPoint(x: 0.0, y: 0.0) 23 | case .topCenter: 24 | CGPoint(x: 0.5, y: 0.0) 25 | case .topRight: 26 | CGPoint(x: 1.0, y: 0.0) 27 | case .midLeft: 28 | CGPoint(x: 0.0, y: 0.5) 29 | case .midCenter: 30 | CGPoint(x: 0.5, y: 0.5) 31 | case .midRight: 32 | CGPoint(x: 1.0, y: 0.5) 33 | case .btmLeft: 34 | CGPoint(x: 0.0, y: 1.0) 35 | case .btmCenter: 36 | CGPoint(x: 0.5, y: 1.0) 37 | case .btmRight: 38 | CGPoint(x: 1.0, y: 1.0) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 ChenYunGui (陈云贵) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/base/BaseDebugger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseDebugger.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/22. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public class BaseDebugger: BaseLogger { 12 | 13 | public var name: String? 14 | public private(set) var transform: Matrix2D 15 | public let color: AppColor 16 | 17 | public init( 18 | name: String? = nil, 19 | transform: Matrix2D, 20 | color: AppColor 21 | ) { 22 | self.name = name 23 | self.transform = transform 24 | self.color = color 25 | } 26 | 27 | public func applying(_ transform: Matrix2D) -> Self { 28 | self.transform = self.transform * transform 29 | return self 30 | } 31 | } 32 | 33 | 34 | public class BaseLogger { 35 | public var logs: [Logger.Log] = [] 36 | 37 | func logging(_ messages: [Any], level: Logger.Log.Level = .info, separator: String = ", ") { 38 | let stringMessage = messages.map { String(reflecting: $0) }.joined(separator: separator) 39 | let log = Logger.Log(message: stringMessage, level: level) 40 | self.logs.append(log) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/coordinateSystem/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | func getDivision(_ value: Double, segments: Int = 5) -> Double { 11 | // 处理输入值为 0 的情况 12 | guard value != 0 else { return 0 } 13 | 14 | // 计算近似步长 15 | let test = abs(value) / Double(segments) 16 | 17 | // 计算数量级的指数 18 | let exp = floor(log10(test)) 19 | 20 | // 计算基数 p = 10^exp 21 | let p = pow(10, exp) 22 | 23 | // 定义“漂亮”数字的倍数 24 | let multipliers = [1.0, 2.0, 2.5, 5.0, 10.0] 25 | 26 | // 生成候选步长 27 | let candidates = multipliers.map { $0 * p } 28 | 29 | // 找到最小的候选步长 >= test 30 | for c in candidates { 31 | if c >= test { 32 | return c 33 | } 34 | } 35 | 36 | // 理论上不会到达这里,但为安全起见返回最后一个候选值 37 | return candidates.last ?? 0 38 | } 39 | 40 | func calculatePrecision(_ value: Double) -> Int { 41 | let exp = log10(value) 42 | if exp < 0 { 43 | return Int(ceil(abs(exp))) + 1 44 | } 45 | return 1 46 | } 47 | 48 | func clockwiseInYDown(v0: CGPoint, v1: CGPoint, v2: CGPoint) -> Bool { 49 | return (v2.x - v0.x) * (v1.y - v2.y) < (v2.y - v0.y) * (v1.x - v2.x) 50 | } 51 | -------------------------------------------------------------------------------- /Sources/VisualUtils/AppColor+Behavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppColor+Behavior.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/7. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | extension AppColor { 11 | private static let colors: [UInt32] = [ 12 | 0x50AADA, 0x8DE050, 0xFFDC58, 0xFFB768, 0xFF4D54, 0x9635AF, 13 | 0x3591C2, 0x5DBB33, 0xF2CB2E, 0xFF9E35, 0xFF1220, 0x63177A, 14 | 0x267298, 0x6BA737, 0xE2AF0F, 0xEF932B, 0xCE0E27, 0x4C0C60, 15 | 0x074D6D, 0x4A7D23, 0xC3880A, 0xD07218, 0xAA0517, 0x360540, 16 | ] 17 | } 18 | 19 | extension AppColor { 20 | public convenience init(rgb: UInt32, alpha: CGFloat = 1) { 21 | let r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 22 | let g = CGFloat((rgb & 0xFF00) >> 8) / 255.0 23 | let b = CGFloat((rgb & 0xFF) >> 0) / 255.0 24 | self.init(red: r, green: g, blue: b, alpha: alpha) 25 | } 26 | 27 | public static var randomColor: AppColor { 28 | let value: UInt32 = colors.randomElement()! 29 | return AppColor(rgb: value, alpha: 1) 30 | } 31 | 32 | public static subscript(_ index: Int) -> AppColor { 33 | let index = index % colors.count 34 | let value = colors[index] 35 | return AppColor(rgb: value, alpha: 1) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/static/TextSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextSource.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/2. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum TextSource { 11 | 12 | case string(String) 13 | case number(Double, formatter: NumberFormatter) 14 | case index(Int) 15 | 16 | // TODO: add date etc... 17 | 18 | public var string: String { 19 | switch self { 20 | case .string(let string): 21 | return string 22 | case .number(let value, formatter: let formatter): 23 | return formatter.string(from: NSNumber(value: value)) ?? "0" 24 | case .index(let value): 25 | return "\(value)" 26 | } 27 | } 28 | } 29 | 30 | extension TextSource { 31 | public static func number(value: Double, precision: Int) -> TextSource { 32 | let formatter = NumberFormatter() 33 | formatter.numberStyle = .decimal 34 | formatter.maximumFractionDigits = precision 35 | formatter.roundingMode = .halfUp 36 | return .number(value, formatter: formatter) 37 | } 38 | } 39 | 40 | extension TextSource: ExpressibleByStringLiteral { 41 | public typealias StringLiteralType = String 42 | 43 | public init(stringLiteral value: StringLiteralType) { 44 | self = .string(value) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/Mesh+Debug.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mesh+Debug.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/3. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | extension Mesh: DebugRenderable { 12 | public var debugBounds: CGRect? { 13 | guard let bounds = vertices.bounds else { return nil } 14 | return bounds * transform 15 | } 16 | 17 | public func render(with transform: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 18 | let matrix = self.transform * transform 19 | // 首先渲染面 20 | // TODO: need to fix 21 | if displayOptions.contains(.face) { 22 | for face in faceElements { 23 | face.render(with: matrix, in: context, scale: scale, contextHeight: contextHeight) 24 | } 25 | } 26 | 27 | // 然后渲染边 28 | if displayOptions.contains(.edge) { 29 | for edge in edgeElements { 30 | edge.render(with: matrix, in: context, scale: scale, contextHeight: contextHeight) 31 | } 32 | } 33 | 34 | // 最后渲染顶点 35 | if displayOptions.contains(.vertex) { 36 | for vertex in vertexElements { 37 | vertex.render(with: matrix, in: context, scale: scale, contextHeight: contextHeight) 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "VisualDebugger", 8 | platforms: [ 9 | .macOS(.v15), 10 | .iOS(.v17), 11 | .macCatalyst(.v17) 12 | ], 13 | products: [ 14 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 15 | .library( 16 | name: "VisualDebugger", 17 | targets: ["VisualDebugger"]), 18 | .library( 19 | name: "VisualUtils", 20 | targets: ["VisualUtils"] 21 | ) 22 | ], 23 | dependencies: [ 24 | // Dependencies declare other packages that this package depends on. 25 | // .package(url: /* package url */, from: "1.0.0"), 26 | ], 27 | targets: [ 28 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 29 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 30 | .target(name: "VisualUtils"), 31 | .target( 32 | name: "VisualDebugger", 33 | dependencies: ["VisualUtils"]), 34 | .testTarget( 35 | name: "VisualDebuggerTests", 36 | dependencies: ["VisualDebugger"]), 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/coordinateSystem/Coordinate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinate.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | 9 | import Foundation 10 | 11 | struct Coordinate { 12 | 13 | let segmentValue: CGFloat 14 | let valueRect: CGRect 15 | let origin: CGPoint 16 | let xAxis: Axis 17 | let yAxis: Axis 18 | 19 | init(rect: CGRect, numSegments: Int) { 20 | let maxValue = max(rect.size.width, rect.size.height) 21 | self.segmentValue = CGFloat(getDivision(Double(maxValue), segments: numSegments)) 22 | let xAxisData = AxisData(min: rect.minX, max: rect.maxX, segmentValue: segmentValue) 23 | let yAxisData = AxisData(min: rect.minY, max: rect.maxY, segmentValue: segmentValue) 24 | self.valueRect = CGRect( 25 | x: xAxisData.startValue, 26 | y: yAxisData.startValue, 27 | width: xAxisData.lengthValue, 28 | height: yAxisData.lengthValue 29 | ) 30 | let originY: CGFloat = (valueRect.minY < 0 && valueRect.maxY >= 0) ? 0 : yAxisData.startValue 31 | let originX: CGFloat = (valueRect.minX < 0 && valueRect.maxX >= 0) ? 0 : xAxisData.startValue 32 | let origin = CGPoint(x: originX, y: originY) 33 | self.xAxis = xAxisData.getAxis(type: .x(origin: origin)) 34 | self.yAxis = yAxisData.getAxis(type: .y(origin: origin)) 35 | self.origin = origin 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/Debuggable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Debuggable.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public protocol ContextRenderable { 12 | var logs: [Logger.Log] { get } 13 | func render( 14 | with transform: Matrix2D, 15 | in context: CGContext, 16 | scale: CGFloat, 17 | contextHeight: Int? 18 | ) 19 | } 20 | 21 | extension ContextRenderable { 22 | public var logs: [Logger.Log] { [] } 23 | } 24 | 25 | public protocol DebugRenderable: ContextRenderable { 26 | var debugBounds: CGRect? { get } 27 | } 28 | 29 | public protocol Debuggable { 30 | var preferredDebugConfig: DebugContext.Config? { get } 31 | var debugElements: [any DebugRenderable] { get } 32 | } 33 | 34 | extension Debuggable { 35 | public var preferredDebugConfig: DebugContext.Config? { 36 | nil 37 | } 38 | 39 | @MainActor 40 | public var debugView: DebugView { 41 | DebugView(elements: debugElements) 42 | } 43 | 44 | public func debugContext( 45 | config: DebugContext.Config = .init() 46 | ) -> DebugContext { 47 | DebugContext( 48 | config: preferredDebugConfig ?? config, 49 | elements: debugElements 50 | ) 51 | } 52 | } 53 | 54 | extension Array where Element == any DebugRenderable { 55 | var debugBounds: CGRect? { 56 | self.compactMap { $0.debugBounds }.bounds 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/TextRenderStyle+Bg.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextRenderStyle+Bg.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/27. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | extension TextRenderStyle { 12 | 13 | public enum BgStyle { 14 | case rect(color: AppColor, filled: Bool) 15 | case roundRect(radius: Double, color: AppColor, filled: Bool) 16 | case capsule(color: AppColor, filled: Bool) 17 | } 18 | 19 | public struct Shadow { 20 | public var color: AppColor 21 | public var offset: CGSize 22 | public var blur: CGFloat 23 | 24 | public init(color: AppColor, offset: CGSize, blur: CGFloat) { 25 | self.color = color 26 | self.offset = offset 27 | self.blur = blur 28 | } 29 | } 30 | } 31 | 32 | 33 | extension TextRenderStyle.BgStyle { 34 | 35 | public var color: AppColor { 36 | switch self { 37 | case .rect(let color, _): 38 | return color 39 | case .roundRect(_, let color, _): 40 | return color 41 | case .capsule(let color, _): 42 | return color 43 | } 44 | } 45 | 46 | public var filled: Bool { 47 | switch self { 48 | case .rect(_, let filled): 49 | filled 50 | case .roundRect(_, _, let filled): 51 | filled 52 | case .capsule(_, let filled): 53 | filled 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/Mesh+Get.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mesh+Get.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/3. 6 | // 7 | 8 | extension Mesh { 9 | 10 | func getVertices() -> [Vertex] { 11 | getVertices(from: self.vertices) 12 | } 13 | 14 | func getMeshFaces() -> [MeshFace] { 15 | faces.enumerated().map { (i, face) in 16 | createFace( 17 | vertices: [vertices[face.v0], vertices[face.v1], vertices[face.v2]], 18 | faceIndex: i 19 | ) 20 | } 21 | } 22 | 23 | // 从Face数据生成唯一的边 24 | static func getEdges(faces: [Face]) -> [Edge] { 25 | // 使用Set来跟踪唯一的边 26 | var edgeSet: Set = Set() 27 | var edges: [Edge] = [] 28 | 29 | // 处理每个面 30 | for face in faces { 31 | // 为这个三角形创建三条边 32 | let edgePairs = [ 33 | (face.v0, face.v1), 34 | (face.v1, face.v2), 35 | (face.v2, face.v0) 36 | ] 37 | 38 | for (start, end) in edgePairs { 39 | // 创建不考虑方向的唯一标识符,总是较小的索引在前 40 | let orderedPair = start < end ? "\(start)-\(end)" : "\(end)-\(start)" 41 | 42 | // 如果这条边是新的,就添加它 43 | if edgeSet.insert(orderedPair).inserted { 44 | let edge = Edge(org: start, dst: end) 45 | edges.append(edge) 46 | } 47 | } 48 | } 49 | 50 | return edges 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/compositions/PointElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PointElement.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/2. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public final class PointElement { 12 | // TODO: support style inherient 13 | 14 | public var shape: StaticRendable 15 | public var label: TextElement? 16 | 17 | public init(shape: StaticRendable, label: TextElement? = nil) { 18 | self.shape = shape 19 | self.label = label 20 | } 21 | } 22 | 23 | extension PointElement: StaticRendable { 24 | public var contentBounds: CGRect { 25 | var bounds = shape.contentBounds 26 | if let label { 27 | bounds = bounds.union(label.contentBounds) 28 | } 29 | return bounds 30 | } 31 | 32 | public func render( 33 | with transform: Matrix2D, 34 | in context: CGContext, 35 | scale: CGFloat, 36 | contextHeight: Int? 37 | ) { 38 | var array: [StaticRendable] = [shape] 39 | if let label { array.append(label) } 40 | for element in array { 41 | element.render( 42 | with: transform, 43 | in: context, 44 | scale: scale, 45 | contextHeight: contextHeight 46 | ) 47 | } 48 | } 49 | 50 | public func clone() -> PointElement { 51 | PointElement(shape: shape.clone(), label: label?.clone()) 52 | } 53 | } 54 | 55 | //public typealias StaticPointElement = StaticRenderElement 56 | public typealias PointRenderElement = StaticRenderElement 57 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/algorithms/DebugRenderBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugBuilder.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/28. 6 | // 7 | 8 | @resultBuilder 9 | public final class DebugBuilder: BaseBuilder { 10 | } 11 | 12 | @resultBuilder 13 | public final class DebugRenderBuilder: BaseBuilder { 14 | } 15 | 16 | @resultBuilder 17 | public final class RenderBuilder: BaseBuilder { 18 | } 19 | 20 | open class BaseBuilder { 21 | public typealias Expression = E 22 | public typealias Component = [E] 23 | 24 | // MARK: - Expression 25 | public static func buildExpression(_ expression: E) -> Component { 26 | return [expression] 27 | } 28 | 29 | // MARK: - Block overload 30 | public static func buildBlock(_ expressions: E...) -> Component { 31 | return Array(expressions) 32 | } 33 | 34 | public static func buildBlock(_ expressions: [E]) -> Component { 35 | return expressions 36 | } 37 | 38 | public static func buildBlock(_ expressions: [E]...) -> Component { 39 | return expressions.flatMap { $0 } 40 | } 41 | 42 | // MARK: - Optional overload 43 | public static func buildOptional(_ expressions: [E]?) -> Component { 44 | return if let expressions { expressions } else { [] } 45 | } 46 | 47 | // MARK: - If-else overload 48 | public static func buildEither(first child: Component) -> Component { 49 | return child 50 | } 51 | 52 | public static func buildEither(second child: Component) -> Component { 53 | return child 54 | } 55 | 56 | // MARK: - Array overload 57 | public static func buildArray(_ components: [E]) -> Component { 58 | return components 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/coordinateSystem/UIBezierPath+Behavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppBezierPath.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | 9 | import Foundation 10 | import CoreGraphics 11 | import VisualUtils 12 | 13 | extension AppBezierPath { 14 | 15 | public static func xArrow(size: Double) -> AppBezierPath { 16 | let half = size / 2 17 | let path = AppBezierPath() 18 | path.move(to: .zero) 19 | path.addLine(to: .init(x: 0, y: -half)) 20 | path.addLine(to: .init(x: size, y: 0)) 21 | path.addLine(to: .init(x: 0, y: half)) 22 | path.addLine(to: .zero) 23 | path.close() 24 | return path 25 | } 26 | 27 | public static func yUpArrow(size: Double) -> AppBezierPath { 28 | let p = xArrow(size: size) 29 | return (p * Matrix2D(rotationAngle: -.pi/2))! 30 | } 31 | 32 | public static func yDownArrow(size: Double) -> AppBezierPath { 33 | let p = xArrow(size: size) 34 | return (p * Matrix2D(rotationAngle: .pi/2))! 35 | } 36 | 37 | public static func xMark(size: Double) -> AppBezierPath { 38 | let path = AppBezierPath() 39 | path.move(to: .zero) 40 | path.addLine(to: .init(x: 0, y: -size)) 41 | return path 42 | } 43 | 44 | public static func yMark(size: Double) -> AppBezierPath { 45 | let path = AppBezierPath() 46 | path.move(to: .zero) 47 | path.addLine(to: .init(x: size, y: 0)) 48 | return path 49 | } 50 | } 51 | 52 | func *(lhs: AppBezierPath, rhs: Matrix2D) -> AppBezierPath? { 53 | guard let cgPath = lhs.cgPath * rhs else { return nil } 54 | return AppBezierPath(cgPath: cgPath) 55 | } 56 | func *(lhs: CGPath, rhs: Matrix2D) -> CGPath? { 57 | var t = rhs 58 | return lhs.copy(using: &t) 59 | } 60 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/data/VectorTriangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VectorTriangle.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/11. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | public typealias VVectorTriangle = VectorTriangle 11 | public struct VectorTriangle { 12 | public struct Segment { 13 | public var start: CGPoint 14 | public var control1: CGPoint 15 | public var control2: CGPoint 16 | public var end: CGPoint 17 | 18 | public init(start: CGPoint, control1: CGPoint, control2: CGPoint, end: CGPoint) { 19 | self.start = start 20 | self.control1 = control1 21 | self.control2 = control2 22 | self.end = end 23 | } 24 | } 25 | 26 | public var segment: Segment 27 | public var vertex: CGPoint 28 | 29 | public init(segment: Segment, vertex: CGPoint) { 30 | self.segment = segment 31 | self.vertex = vertex 32 | } 33 | 34 | /// 计算三角形的边界框,包含贝塞尔曲线段和顶点 35 | public var bounds: CGRect { 36 | // 收集所有控制点和顶点 37 | let points = [ 38 | segment.start, 39 | segment.control1, 40 | segment.control2, 41 | segment.end, 42 | vertex 43 | ] 44 | 45 | // 计算边界框 46 | if let pointsBounds = points.bounds { 47 | return pointsBounds 48 | } 49 | 50 | // 如果无法计算边界框(例如,没有点),返回零矩形 51 | return .zero 52 | } 53 | 54 | /// 判断三角形是否为逆时针方向 55 | /// 使用曲线的起点、终点和顶点来计算 56 | public var isCCW: Bool { 57 | // 创建三角形的顶点数组 58 | let points = [segment.start, segment.end, vertex] 59 | 60 | // 使用 polyIsCCW 方法计算方向 61 | return points.polyIsCCW 62 | } 63 | 64 | /// 获取方向指示符号 65 | public var orientationSymbol: String { 66 | isCCW ? "↺" : "↻" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/coordinateSystem/AxisData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AxisData.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import CoreGraphics 9 | import Foundation 10 | 11 | struct AxisData { 12 | public static let sideRatio = 0.6 13 | public static let maxSideLength: Double = 20 14 | 15 | // start value of the axis 16 | var startValue: CGFloat 17 | // length value of the axis 18 | var lengthValue: CGFloat 19 | // the value of each segment 20 | var segmentValue: CGFloat 21 | // number segments of starting (reach to 0) 22 | var startSegments: Int 23 | // total segments 24 | var numSegments: Int 25 | 26 | /// the origin value in the coordinate system 27 | var originValue: CGFloat { 28 | return self.startValue + self.segmentValue * CGFloat(self.startSegments) 29 | } 30 | 31 | /// end value of the axis 32 | var endValue: CGFloat { 33 | return self.startValue + self.lengthValue 34 | } 35 | 36 | var marks: [Double] { 37 | (0...numSegments).map{ 38 | startValue + Double($0) * segmentValue 39 | } 40 | } 41 | 42 | init(min minValue: CGFloat, max maxValue: CGFloat, segmentValue: CGFloat) { 43 | self.startValue = floor(minValue / segmentValue) * segmentValue 44 | self.segmentValue = segmentValue 45 | self.startSegments = Int(ceil(abs(self.startValue) / segmentValue)) 46 | self.numSegments = Int(ceil((maxValue - self.startValue) / segmentValue)) 47 | self.lengthValue = segmentValue * CGFloat(self.numSegments) 48 | } 49 | 50 | func getAxis(type: Axis.Kind) -> Axis { 51 | Axis( 52 | type: type, 53 | start: startValue, 54 | end: endValue, 55 | marks: marks, 56 | side: min(segmentValue * Self.sideRatio, Self.maxSideLength) 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/face/FaceRenderElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FaceRenderElement.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/7. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public final class FaceRenderElement: ContextRenderable { 12 | 13 | public let points: [CGPoint] 14 | public let transform: Matrix2D 15 | public let label: TextElement? 16 | public let style: ShapeRenderStyle 17 | 18 | lazy var path: AppBezierPath = { 19 | let path = AppBezierPath() 20 | path.move(to: points[0]) 21 | for i in 1 ..< points.count { 22 | path.addLine(to: points[i]) 23 | } 24 | path.close() 25 | return path 26 | }() 27 | lazy var center: CGPoint = { 28 | points.gravityCenter 29 | }() 30 | 31 | public init(points: [CGPoint], transform: Matrix2D = .identity, style: ShapeRenderStyle, label: TextElement? = nil) { 32 | self.points = points 33 | self.transform = transform 34 | self.label = label 35 | self.style = style 36 | } 37 | 38 | public func render(with matrix: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 39 | guard !points.isEmpty else { return } 40 | let transform = self.transform * matrix 41 | if let newPath = self.path * transform { 42 | context.render(path: newPath.cgPath, style: style) 43 | } 44 | label?.render( 45 | with: Matrix2D(translation: center) * matrix, 46 | in: context, 47 | scale: scale, 48 | contextHeight: contextHeight 49 | ) 50 | } 51 | } 52 | 53 | public func *(lhs: FaceRenderElement, rhs: Matrix2D) -> FaceRenderElement { 54 | FaceRenderElement( 55 | points: lhs.points, 56 | transform: lhs.transform * rhs, 57 | style: lhs.style, 58 | label: lhs.label 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/coordinateSystem/Axis.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Axis.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Axis { 11 | enum Kind { 12 | case x(origin: CGPoint) // x axis with origin 13 | case y(origin: CGPoint) // y axis with origin 14 | } 15 | struct Mark { 16 | enum Kind { 17 | case x, y 18 | } 19 | var type: Kind 20 | var position: CGPoint 21 | 22 | var value: Double { 23 | switch self.type { 24 | case .x: position.x 25 | case .y: position.y 26 | } 27 | } 28 | 29 | init(type: Kind, position: CGPoint) { 30 | self.type = type 31 | self.position = position 32 | } 33 | } 34 | 35 | var type: Kind 36 | var start: Mark 37 | var origin: Mark 38 | var end: Mark 39 | var marks: [Mark] 40 | 41 | init(type: Kind, start: Double, end: Double, marks: [Double], side: Double) { 42 | self.type = type 43 | switch type { 44 | case .x(let center): 45 | let y = center.y 46 | self.start = Mark(type: .x, position: .init(x: start - side, y: y)) 47 | self.origin = Mark(type: .x, position: center) 48 | self.end = Mark(type: .x, position: .init(x: end + side, y: y)) 49 | self.marks = marks.map { Mark(type: .x, position: .init(x: $0, y: y)) } 50 | case .y(let center): 51 | let x = center.x 52 | self.start = Mark(type: .y, position: .init(x: x, y: start - side)) 53 | self.origin = Mark(type: .y, position: center) 54 | self.end = Mark(type: .y, position: .init(x: x, y: end + side)) 55 | self.marks = marks.map { Mark(type: .y, position: .init(x: x, y: $0)) } 56 | } 57 | } 58 | 59 | var estimateMaxLabelWidth: CGFloat? { 60 | self.marks.compactMap { $0.estimateSize().width }.max() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/static/StaticRenderElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarkRenderElement.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public struct StaticRenderElement: DebugRenderable { 12 | 13 | public let content: Content 14 | public let position: CGPoint // this is raw position, transform will no affect this 15 | public let transform: Matrix2D 16 | 17 | public init(content: Content, position: CGPoint, transform: Matrix2D = .identity) { 18 | self.content = content 19 | self.position = position 20 | self.transform = transform 21 | } 22 | 23 | public var debugBounds: CGRect? { 24 | let pos = position * transform 25 | return self.content.contentBounds.offseted(pos) 26 | } 27 | 28 | public func render(with matrix: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 29 | self.content.render( 30 | with: Matrix2D(translation: position) * transform * matrix, 31 | in: context, 32 | scale: scale, 33 | contextHeight: contextHeight 34 | ) 35 | } 36 | } 37 | 38 | extension StaticRenderElement where Content == ShapeElement { 39 | public init(source: ShapeRenderer, style: ShapeRenderStyle, position: CGPoint, transform: Matrix2D = .identity) { 40 | self.content = ShapeElement(renderer: source, style: style) 41 | self.position = position 42 | self.transform = transform 43 | } 44 | } 45 | 46 | extension StaticRenderElement where Content == TextElement { 47 | public init(source: TextSource, style: TextRenderStyle, position: CGPoint, transform: Matrix2D = .identity) { 48 | self.content = TextElement(source: source, style: style) 49 | self.position = position 50 | self.transform = transform 51 | } 52 | } 53 | 54 | public typealias StaticShapeElement = StaticRenderElement 55 | public typealias StaticTextElement = StaticRenderElement 56 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/coordinateSystem/CoordinateRenderElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinateRenderElement.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/4. 6 | // 7 | 8 | import CoreGraphics 9 | #if canImport(UIKit) 10 | import UIKit 11 | #elseif canImport(AppKit) 12 | import AppKit 13 | #endif 14 | import VisualUtils 15 | 16 | final class CoordinateRenderElement: ContextRenderable { 17 | 18 | let coordinate: Coordinate 19 | let coordSystem: CoordinateSystem2D 20 | let style: CoordinateStyle 21 | 22 | lazy var xAxis = AxisRenderElement(axis: coordinate.xAxis, color: style.xAxisColor, coord: coordSystem) 23 | lazy var yAxis = AxisRenderElement(axis: coordinate.yAxis, color: style.yAxisColor, coord: coordSystem) 24 | lazy var origin = getOriginElement() 25 | 26 | init( 27 | coordinate: Coordinate, 28 | coordSystem: CoordinateSystem2D, 29 | style: CoordinateStyle 30 | ) { 31 | self.coordinate = coordinate 32 | self.coordSystem = coordSystem 33 | self.style = style 34 | } 35 | 36 | func render( 37 | with transform: Matrix2D, 38 | in context: CGContext, 39 | scale: CGFloat, 40 | contextHeight: Int? 41 | ) { 42 | xAxis.render(with: transform, in: context, scale: scale, contextHeight: contextHeight) 43 | yAxis.render(with: transform, in: context, scale: scale, contextHeight: contextHeight) 44 | origin.render(with: transform, in: context, scale: scale, contextHeight: contextHeight) 45 | } 46 | 47 | private func getOriginElement() -> StaticRenderElement { 48 | let shapeStyle = ShapeRenderStyle(fill: .init(color: style.originColor)) 49 | let shape = ShapeElement(renderer: Circle(radius: 1), style: shapeStyle) 50 | var labelStyle = TextRenderStyle.originLabel 51 | labelStyle.textColor = style.originColor 52 | let label = TextElement(source: .string("O"), style: labelStyle) 53 | let point = PointElement(shape: shape, label: label) 54 | return .init(content: point, position: coordinate.origin) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/VisualUtils/CGContext+Behavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGContext+Behavior.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | public func withImageContext( 11 | width w: CGFloat, 12 | height h: CGFloat, 13 | scale: CGFloat = 1, 14 | bgColor: CGColor? = nil, 15 | useDeviceSpace: Bool = true, 16 | colorSpace: CGColorSpace? = nil, 17 | byteOrderInfo: CGImageByteOrderInfo = .orderDefault, 18 | alphaInfo: CGImageAlphaInfo = .premultipliedLast, 19 | _ handler: (CGContext) throws -> Void 20 | ) rethrows -> CGImage? { 21 | let width = Int(w * scale) 22 | let height = Int(h * scale) 23 | let rect = CGRect(x: 0, y: 0, width: width, height: height) 24 | 25 | let pixels = UnsafeMutablePointer.allocate(capacity: width * height * 4) 26 | defer { pixels.deallocate() } 27 | 28 | var _colorSpace = colorSpace 29 | if _colorSpace == nil { 30 | if alphaInfo == .alphaOnly { 31 | _colorSpace = CGColorSpaceCreateDeviceGray() 32 | } else { 33 | _colorSpace = CGColorSpaceCreateDeviceRGB() 34 | } 35 | } 36 | let bitmapInfo = CGBitmapInfo(rawValue: byteOrderInfo.rawValue | alphaInfo.rawValue) 37 | 38 | guard 39 | let context = CGContext( 40 | data: pixels, 41 | width: width, 42 | height: height, 43 | bitsPerComponent: 8, 44 | bytesPerRow: width * 4, 45 | space: _colorSpace!, 46 | bitmapInfo: bitmapInfo.rawValue 47 | ) 48 | else { 49 | return nil 50 | } 51 | 52 | context.clear(rect) 53 | 54 | if let bgColor = bgColor { 55 | context.saveGState() 56 | context.setFillColor(bgColor) 57 | context.fill(rect) 58 | context.restoreGState() 59 | } 60 | 61 | context.interpolationQuality = .high 62 | context.setShouldSmoothFonts(true) 63 | if useDeviceSpace { 64 | context.concatenate(context.userSpaceToDeviceSpaceTransform) 65 | } 66 | context.concatenate(CGAffineTransform(scaleX: scale, y: scale)) 67 | 68 | try handler(context) 69 | return context.makeImage() 70 | } 71 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/static/ShapeElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapeElement.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/2. 6 | // 7 | 8 | import Foundation 9 | import CoreGraphics 10 | import VisualUtils 11 | 12 | public final class ShapeElement: StaticRendable { 13 | 14 | public var renderer: ShapeRenderer 15 | public var style: ShapeRenderStyle 16 | public var rotatable: Bool 17 | 18 | public init(renderer: ShapeRenderer, style: ShapeRenderStyle, rotatable: Bool = false) { 19 | self.renderer = renderer 20 | self.style = style 21 | self.rotatable = rotatable 22 | } 23 | 24 | public var contentBounds: CGRect { 25 | renderer.bounds 26 | } 27 | 28 | public func render( 29 | with transform: Matrix2D, 30 | in context: CGContext, 31 | scale: CGFloat, 32 | contextHeight: Int? 33 | ) { 34 | var t = Matrix2D(translationX: transform.tx, y: transform.ty) 35 | if rotatable { 36 | t = Matrix2D(rotationAngle: transform.decompose().rotation) * t 37 | } 38 | context.render( 39 | path: renderer.getBezierPath().cgPath, 40 | style: style, 41 | transform: t 42 | ) 43 | } 44 | 45 | public func clone() -> ShapeElement { 46 | ShapeElement(renderer: renderer, style: style, rotatable: rotatable) 47 | } 48 | } 49 | 50 | extension CGContext { 51 | 52 | public func render( 53 | path cgPath: CGPath, 54 | style: ShapeRenderStyle, 55 | transform: Matrix2D 56 | ) { 57 | guard !cgPath.isEmpty, !style.isEmpty else { return } 58 | var t = transform 59 | guard let p = cgPath.copy(using: &t) else { return } 60 | 61 | self.saveGState() 62 | defer { self.restoreGState() } 63 | // fill 64 | if let fill = style.fill { 65 | self.addPath(p) 66 | fill.set(for: self) 67 | self.fillPath(using: fill.rule) 68 | } 69 | 70 | // stroke 71 | if let stroke = style.stroke { 72 | self.addPath(p) 73 | stroke.set(for: self) 74 | self.strokePath() 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/Path.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Path.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/16. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | #elseif canImport(AppKit) 11 | import AppKit 12 | #endif 13 | import VisualUtils 14 | 15 | public typealias VPath = Path 16 | 17 | public final class Path: BaseDebugger { 18 | 19 | public enum PathStyle { 20 | case stroke(width: CGFloat = 1, dashed: Bool = false) 21 | case fill 22 | } 23 | 24 | public let path: AppBezierPath 25 | public let element: ShapeRenderElement 26 | 27 | public init( 28 | path: AppBezierPath, 29 | name: String? = nil, 30 | transform: Matrix2D = .identity, 31 | color: AppColor = .yellow, 32 | style: PathStyle = .stroke() 33 | ) { 34 | self.path = path 35 | let style: ShapeRenderStyle = switch style { 36 | case .stroke(let width, let dashed): 37 | .init(stroke: .init(color: color, style: .init(lineWidth: width, dash: dashed ? [5, 5] : [] ))) 38 | case .fill: 39 | .init(fill: .init(color: color)) 40 | } 41 | self.element = ShapeRenderElement(path: path, style: style) 42 | super.init(name: name, transform: transform, color: color) 43 | } 44 | 45 | public func log(_ message: Any..., level: Logger.Log.Level = .info) -> Self { 46 | self.logging(message, level: level) 47 | return self 48 | } 49 | } 50 | 51 | extension Path: DebugRenderable { 52 | 53 | public var debugBounds: CGRect? { 54 | if let bounds = element.debugBounds { 55 | return bounds * transform 56 | } else { 57 | return nil 58 | } 59 | } 60 | 61 | public func render(with transform: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 62 | element.render(with: self.transform * transform, in: context, scale: scale, contextHeight: contextHeight) 63 | } 64 | } 65 | 66 | #Preview(traits: .fixedLayout(width: 400, height: 420)) { 67 | DebugView { 68 | Path(path: AppBezierPath(rect: .init(x: 0, y: 0, width: 100, height: 100))) 69 | Path(path: AppBezierPath(ovalIn: .init(x: 20, y: 20, width: 60, height: 60))) 70 | } 71 | .coordinateVisible(true) 72 | .coordinateStyle(.default) 73 | .coordinateSystem(.yDown) 74 | } 75 | 76 | 77 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/face/VectorTriangleRenderElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VectorTriangleRenderElement.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/11. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public final class VectorTriangleRenderElement: ContextRenderable { 12 | 13 | public let triangle: VectorTriangle 14 | public let transform: Matrix2D 15 | public let label: TextElement? 16 | public let style: ShapeRenderStyle 17 | 18 | lazy var path: AppBezierPath = { 19 | let path = AppBezierPath() 20 | // 从贝塞尔曲线段的起点开始 21 | path.move(to: triangle.segment.start) 22 | // 添加贝塞尔曲线 23 | path.addCurve(to: triangle.segment.end, 24 | controlPoint1: triangle.segment.control1, 25 | controlPoint2: triangle.segment.control2) 26 | // 连接到顶点 27 | path.addLine(to: triangle.vertex) 28 | // 闭合路径 29 | path.close() 30 | return path 31 | }() 32 | 33 | lazy var center: CGPoint = { 34 | // 计算三角形的重心 35 | let p0 = triangle.segment.start 36 | let p3 = triangle.segment.end 37 | let v = triangle.vertex 38 | return (p0 + p3 + v) / 3.0 39 | }() 40 | 41 | public init(triangle: VectorTriangle, transform: Matrix2D = .identity, style: ShapeRenderStyle, label: TextElement? = nil) { 42 | self.triangle = triangle 43 | self.transform = transform 44 | self.label = label 45 | self.style = style 46 | } 47 | 48 | public func applying(transform: Matrix2D) -> VectorTriangleRenderElement { 49 | self * transform 50 | } 51 | 52 | public func render(with matrix: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 53 | let transform = self.transform * matrix 54 | if let newPath = self.path * transform { 55 | context.render(path: newPath.cgPath, style: style) 56 | } 57 | label?.render( 58 | with: Matrix2D(translation: center) * matrix, 59 | in: context, 60 | scale: scale, 61 | contextHeight: contextHeight 62 | ) 63 | } 64 | } 65 | 66 | public func *(lhs: VectorTriangleRenderElement, rhs: Matrix2D) -> VectorTriangleRenderElement { 67 | VectorTriangleRenderElement( 68 | triangle: lhs.triangle, 69 | transform: lhs.transform * rhs, 70 | style: lhs.style, 71 | label: lhs.label 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Copilot 指南 — VisualDebugger 2 | 3 | 目的:帮助 AI 编码代理快速上手本仓库,理解关键架构、约定、构建与常见样例。 4 | 5 | - **仓库类型**:Swift 多目标包(SPM),同时提供 CocoaPods 支持。主要 targets:`VisualDebugger`, `VisualUtils`(见 `Package.swift`)。 6 | - **主要入口/组件**: 7 | - `DebugView` / `Sources/VisualDebugger/DebugView.swift`:UI 层,负责平台差异(iOS/macOS)绘制与刷新。 8 | - `DebugContext` / `Sources/VisualDebugger/DebugContext.swift`:渲染上下文与坐标变换、元素合并、日志渲染逻辑。 9 | - `DebugManager` / `Sources/VisualDebugger/DebugManager.swift`:全局元素单例,按队列同步追加并在最终渲染时合并。 10 | - `DebugCapture` / `Sources/VisualDebugger/DebugCapture.swift`:批量截图、写入文件、提供 `shared` 快照流水线(含线程锁保护)。 11 | - `Debuggable`/`DebugRenderable`/`ContextRenderable` / `Sources/VisualDebugger/Debuggable.swift`:核心协议,定义渲染契约与 `debugElements`/`preferredDebugConfig` 的约定。 12 | - `Logger` / `Sources/VisualDebugger/debuggers/Logger.swift`:内部日志系统(单例、线程安全),日志渲染为文本元素。 13 | 14 | - **重要模式与约定(项目特有)**: 15 | - 使用 Swift `@resultBuilder`(`DebugBuilder`、`DebugRenderBuilder`、`RenderBuilder`)构造渲染元素。示例见 `algorithms/DebugRenderBuilder.swift`。 16 | - 渲染元素实现 `ContextRenderable` 或 `DebugRenderable`,并通过 `DebugView(elements:)` 或 `DebugContext(elements:)` 进行绘制。 17 | - 单例与线程安全:`DebugManager.shared` 和 `Logger.default` 都通过私有队列保证同步;`DebugCapture.shared` 通过 `NSLock` 保护静态共享实例。 18 | - 坐标系显式支持 `.yDown` / `.yUp`(参见 `coordinateSystem/CoordinateSystem2D.swift`),渲染流程会根据 `coordinateSystem` 做翻转矩阵处理。 19 | - 平台差异:`DebugView.draw(_:)` 在 iOS 使用 `UIGraphicsGetCurrentContext()`,在 macOS 使用 `NSGraphicsContext.current?.cgContext`;注意 `isFlipped` 在 macOS 的影响。 20 | 21 | - **构建 / 测试 / 调试**: 22 | - Swift Package: `swift build`,`swift test`(在 macOS / CI 环境运行)。 23 | - Xcode workspace: `open VisualDebugger.xcworkspace`(仓库内包含 workspace 与 Playground 示例)。 24 | - CocoaPods: 编辑 `Podfile` 后 `pod install`;仓库提供 `VisualDebugger.podspec`,可用 `pod lint` / `pod trunk push`。 25 | - 运行 Playground: 打开 `VisualDebugger.xcworkspace` 下的 `Playground.playground` 页面,里面有多个演示页面用于快速验证视觉输出。 26 | 27 | - **常见开发任务 & 代码示例(可直接用于自动化修改/补全)**: 28 | - 创建一个简单调试视图: 29 | - `DebugView { Polygon([...]) }` 或 `DebugView(elements: [myDebugElement])`(参见 `README.md` 示例)。 30 | - 在算法执行中拍摄快照:创建 `DebugCapture.shared = DebugCapture(context: ctx, folder: url)`,然后调用 `captureElements("step") { Polygon(...) }`,最后 `output()` 写入文件。 31 | - 自定义元素:实现 `DebugRenderable`,提供 `debugBounds` 与 `render(with:in:scale:contextHeight:)`。 32 | 33 | - **代码风格 / 命名约定**: 34 | - 含平台条件编译(`#if canImport(UIKit)` / `#elseif canImport(AppKit)`)以保持 iOS/macOS 兼容。 35 | - 使用 `@unchecked Sendable` 在需要时标注类型以跨线程使用(注意确保内部同步)。 36 | - result-builder 返回数组类型:`BaseBuilder` 提供了多种 `buildBlock`/`buildOptional` 重载,代理应按此模式生成元素集合。 37 | 38 | - **注意事项(代理在修改/生成代码时要留意)**: 39 | - 渲染尺寸由 `DebugContext` 在初始化时计算(基于 `elements.debugBounds`),修改元素集合可能需要同步更新 `DebugView.frame`。 40 | - 不要移除或更改线程保护(`DispatchQueue`/`NSLock`)否则可能引起并发问题。 41 | - 在添加依赖或修改 API 时,同步更新 `Package.swift` 与 `VisualDebugger.podspec`。 42 | 43 | 如果需要,我可以把本文件调整为英文版本,或把更多示例(函数签名和更细的类间调用序列)加入到说明中。请告知哪些部分需要更详细的示例或补充。 44 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/coordinateSystem/Styles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextRenderStyle.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | 9 | import Foundation 10 | #if canImport(UIKit) 11 | import UIKit 12 | #elseif canImport(AppKit) 13 | import AppKit 14 | #endif 15 | import VisualUtils 16 | 17 | extension TextRenderStyle { 18 | 19 | static let `default` = TextRenderStyle( 20 | font: AppFont.systemFont(ofSize: 10), 21 | insets: .zero, 22 | margin: .zero, 23 | anchor: .topLeft, 24 | textColor: AppColor.white 25 | ) 26 | 27 | static let xAxisLabel = TextRenderStyle( 28 | font: AppFont(name: "HelveticaNeueInterface-Thin", size: 10) ?? AppFont.systemFont(ofSize: 10), 29 | insets: .zero, 30 | margin: AppEdgeInsets(top: 2, left: 2, bottom: 2, right: 2), 31 | anchor: .topCenter, 32 | textColor: AppColor.lightGray 33 | ) 34 | 35 | static let yAxisLabel = TextRenderStyle( 36 | font: AppFont(name: "HelveticaNeueInterface-Thin", size: 10) ?? AppFont.systemFont(ofSize: 10), 37 | insets: .zero, 38 | margin: AppEdgeInsets(top: 2, left: 2, bottom: 2, right: 2), 39 | anchor: .midRight, 40 | textColor: AppColor.lightGray 41 | ) 42 | 43 | static let originLabel = TextRenderStyle( 44 | font: AppFont.italicSystemFont(ofSize: 10), 45 | insets: .zero, 46 | margin: AppEdgeInsets(top: 2, left: 2, bottom: 2, right: 2), 47 | anchor: .topRight, 48 | textColor: AppColor.lightGray 49 | ) 50 | 51 | static let indexLabel = TextRenderStyle( 52 | font: AppFont.italicSystemFont(ofSize: 10), 53 | insets: .zero, 54 | margin: AppEdgeInsets(top: 2, left: 2, bottom: 2, right: 2), 55 | anchor: .midCenter, 56 | textColor: AppColor.white, 57 | bgStyle: .capsule(color: .gray, filled: true) 58 | ) 59 | 60 | public static let nameLabel = TextRenderStyle( 61 | font: AppFont.italicSystemFont(ofSize: 10), 62 | insets: .zero, 63 | margin: AppEdgeInsets(top: 2, left: 10, bottom: 2, right: 2), 64 | anchor: .midLeft, 65 | textStroke: .init(color: .black, width: -30), 66 | textColor: AppColor.white 67 | ) 68 | 69 | static let edgeNameLabel = TextRenderStyle( 70 | font: AppFont.italicSystemFont(ofSize: 10), 71 | insets: .zero, 72 | margin: AppEdgeInsets(top: 2, left: 10, bottom: 2, right: 2), 73 | anchor: .midCenter, 74 | textColor: AppColor.white 75 | ) 76 | } 77 | 78 | extension ShapeRenderStyle { 79 | 80 | static let axis = ShapeRenderStyle( 81 | stroke: Stroke(color: AppColor.lightGray.withAlphaComponent(0.5), style: .init(lineWidth: 1)) 82 | ) 83 | static let arrow = ShapeRenderStyle( 84 | fill: Fill(color: AppColor.lightGray.withAlphaComponent(0.5), style: .init()) 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/static/shapes/PolygonShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Polygon.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/28. 6 | // 7 | 8 | 9 | import CoreGraphics 10 | #if canImport(UIKit) 11 | import UIKit 12 | #elseif canImport(AppKit) 13 | import AppKit 14 | #endif 15 | import VisualUtils 16 | 17 | public struct PolygonShape: ShapeRenderer { 18 | public var center: CGPoint 19 | public var radius: Double 20 | public var edgeCount: Int 21 | 22 | public var bounds: CGRect { 23 | CGRect(center: center, size: .init(width: radius * 2, height: radius * 2)) 24 | } 25 | 26 | public init(center: CGPoint = .zero, radius: Double, edgeCount: Int) { 27 | self.center = center 28 | self.radius = radius 29 | self.edgeCount = edgeCount 30 | } 31 | 32 | public init(center: CGPoint = .zero, edgeLength: Double, edgeCount: Int) { 33 | self.center = center 34 | self.edgeCount = edgeCount 35 | 36 | // 确保边数至少为3(三角形) 37 | let sides = max(edgeCount, 3) 38 | 39 | // 计算内切圆半径 40 | // 对于正多边形,边长与内切圆半径的关系是: 41 | // radius = edgeLength / (2 * sin(π/sides)) 42 | let angle = Double.pi / Double(sides) 43 | self.radius = edgeLength / (2 * sin(angle)) 44 | } 45 | 46 | public func getBezierPath() -> AppBezierPath { 47 | // 创建一个新的 UIBezierPath 48 | let path = AppBezierPath() 49 | 50 | // 确保边数至少为3(三角形) 51 | let sides = max(edgeCount, 3) 52 | 53 | // 计算每个顶点之间的角度 54 | let angle = 2.0 * Double.pi / Double(sides) 55 | 56 | // 计算初始角度 57 | let startAngle: Double 58 | 59 | if sides % 2 == 1 { 60 | // 奇数边形:第一个顶点在顶部中心 (270度或-90度,因为Y轴向下为正) 61 | startAngle = -Double.pi / 2.0 62 | } else { 63 | // 偶数边形:第一条边在顶部中心 64 | // 需要将多边形旋转半个角度,使第一条边水平对齐顶部 65 | startAngle = -Double.pi / 2.0 - angle / 2.0 66 | } 67 | 68 | // 计算第一个点的坐标并设置为起点 69 | let firstX = center.x + CGFloat(radius * cos(startAngle)) 70 | let firstY = center.y + CGFloat(radius * sin(startAngle)) 71 | path.move(to: CGPoint(x: firstX, y: firstY)) 72 | 73 | // 计算并连接所有其他顶点 74 | for i in 1.. PolygonShape { 90 | return PolygonShape(center: center, radius: radius, edgeCount: edgeCount) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /VisualDebugger.xcworkspace/Playground.playground/Pages/TestRects.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import CoreGraphics 5 | import VisualDebugger 6 | import UIKit 7 | 8 | //let string = "395.857385,691.871833 416.205258,691.871833 440.74285,677.502882 464.025071,653.64645 485.086349,632.065736 502.512843,605.126383 508.983172,584.394425 513.066052,571.31223 515.034109,548.768658 515.426052,518.339778 515.608826,504.149949 515.462673,490.258903 515.048626,471.060515 515.074262,472.249217 514.769837,458.517865 514.710399,455.422465 513.454395,390.012333 460.716218,337.430968 395.857385,337.430968 331.866685,337.430968 280.263459,389.318475 276.262562,455.484204 273.900628,494.545204 276.610344,558.923859 283.498406,584.450528 289.259337,605.80013 306.223152,632.714156 327.516576,654.355108 350.507923,677.721694 375.277583,691.871833 395.857385,691.871833" 9 | // 10 | //let lines = string.split(separator: " ") 11 | //var points = [String]() 12 | //for line in lines { 13 | // let words = line.split(separator: ",") 14 | // let x = words[0] 15 | // let y = words[1] 16 | // points.append("CGPoint(x: \(x), y: \(y))") 17 | //} 18 | // 19 | //print(points.joined(separator: ",")) 20 | 21 | let points: [CGPoint] = [CGPoint(x: 395.857385, y: 691.871833),CGPoint(x: 416.205258, y: 691.871833),CGPoint(x: 440.74285, y: 677.502882),CGPoint(x: 464.025071, y: 653.64645),CGPoint(x: 485.086349, y: 632.065736),CGPoint(x: 502.512843, y: 605.126383),CGPoint(x: 508.983172, y: 584.394425),CGPoint(x: 513.066052, y: 571.31223),CGPoint(x: 515.034109, y: 548.768658),CGPoint(x: 515.426052, y: 518.339778),CGPoint(x: 515.608826, y: 504.149949),CGPoint(x: 515.462673, y: 490.258903),CGPoint(x: 515.048626, y: 471.060515),CGPoint(x: 515.074262, y: 472.249217),CGPoint(x: 514.769837, y: 458.517865),CGPoint(x: 514.710399, y: 455.422465),CGPoint(x: 513.454395, y: 390.012333),CGPoint(x: 460.716218, y: 337.430968),CGPoint(x: 395.857385, y: 337.430968),CGPoint(x: 331.866685, y: 337.430968),CGPoint(x: 280.263459, y: 389.318475),CGPoint(x: 276.262562, y: 455.484204),CGPoint(x: 273.900628, y: 494.545204),CGPoint(x: 276.610344, y: 558.923859),CGPoint(x: 283.498406, y: 584.450528),CGPoint(x: 289.259337, y: 605.80013),CGPoint(x: 306.223152, y: 632.714156),CGPoint(x: 327.516576, y: 654.355108),CGPoint(x: 350.507923, y: 677.721694),CGPoint(x: 375.277583, y: 691.871833),CGPoint(x: 395.857385, y: 691.871833)] 22 | 23 | let path = UIBezierPath() 24 | path.move(to: points[0]) 25 | for i in stride(from: 1, to: points.count, by: 3) { 26 | let p1 = points[i] 27 | let p2 = points[i+1] 28 | let p3 = points[i+2] 29 | path.addCurve(to: p3, controlPoint1: p1, controlPoint2: p2) 30 | } 31 | path.debugView 32 | 33 | let bounds = points.bounds 34 | var newPoints = [CGPoint]() 35 | for point in points { 36 | let offseted = CGPoint(x: point.x - bounds.origin.x, y: point.y - bounds.origin.y) 37 | let scaled = CGPoint(x: offseted.x * (1/bounds.width), y: offseted.y * (1/bounds.height)) 38 | newPoints.append(scaled) 39 | } 40 | 41 | let final = newPoints.map{ 42 | return "\($0.x), \($0.y)" 43 | }.joined(separator: "\n") 44 | print(final) 45 | 46 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/static/TextElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextElement.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/2. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public final class TextElement: StaticRendable { 12 | 13 | public var source: TextSource 14 | public var style: TextRenderStyle 15 | public var rotatable: Bool 16 | 17 | public init(source: TextSource, style: TextRenderStyle, rotatable: Bool = false) { 18 | self.source = source 19 | self.style = style 20 | self.rotatable = rotatable 21 | } 22 | 23 | public init(text: String, style: TextRenderStyle, rotatable: Bool = false) { 24 | self.source = .string(text) 25 | self.style = style 26 | self.rotatable = rotatable 27 | } 28 | 29 | public var contentBounds: CGRect { 30 | let size = self.style.getTextSize(text: source.string) 31 | return CGRect(anchor: style.anchor, center: .zero, size: size) 32 | } 33 | 34 | public func render( 35 | with transform: Matrix2D, 36 | in context: CGContext, 37 | scale: CGFloat, 38 | contextHeight: Int? 39 | ) { 40 | var t = Matrix2D(translationX: transform.tx, y: transform.ty) 41 | if rotatable { 42 | var angle = transform.decompose().rotation 43 | angle = convertToReadableAngle(angle) 44 | t = Matrix2D(rotationAngle: angle) * t 45 | } 46 | context.render( 47 | text: source.string, 48 | transform: t, 49 | style: style, 50 | scale: scale, 51 | contextHeight: contextHeight 52 | ) 53 | } 54 | 55 | public func clone() -> TextElement { 56 | TextElement(source: source, style: style, rotatable: rotatable) 57 | } 58 | 59 | /** 60 | 将任意弧度转换为适合阅读的弧度(-π/2到π/2之间) 61 | 62 | 根据阅读习惯,我们通常从左往右阅读文字,即弧度在 -π/2到π/2之间。 63 | 当文字弧度超出这个范围时,需要旋转π弧度使其可读。 64 | 65 | - Parameter angle: 原始弧度 66 | - Returns: 转换后的适合阅读的弧度 67 | */ 68 | func convertToReadableAngle(_ angle: Double) -> Double { 69 | // 首先将弧度标准化到 [-π, π] 范围内 70 | let π = Double.pi 71 | var normalizedAngle = angle.truncatingRemainder(dividingBy: 2 * π) 72 | 73 | if normalizedAngle > π { 74 | normalizedAngle -= 2 * π 75 | } else if normalizedAngle < -π { 76 | normalizedAngle += 2 * π 77 | } 78 | 79 | // 检查弧度是否在 [-π/2, π/2] 范围内 80 | // 如果不是,则旋转π弧度 81 | if normalizedAngle > π/2 && normalizedAngle <= π { 82 | normalizedAngle -= π 83 | } else if normalizedAngle < -π/2 && normalizedAngle >= -π { 84 | normalizedAngle += π 85 | } 86 | return normalizedAngle 87 | } 88 | } 89 | 90 | extension TextElement { 91 | public static func label(_ label: String, at location: TextLocation = .right) -> TextElement { 92 | var style = TextRenderStyle.nameLabel 93 | style.setTextLocation(location) 94 | return TextElement(source: .string(label), style: style) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/Dot.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Point.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/5. 6 | // 7 | 8 | // 9 | // Dot.swift 10 | // VisualDebugger 11 | // 12 | // Created by chenyungui on 2025/3/5. 13 | // 14 | 15 | import CoreGraphics 16 | #if canImport(UIKit) 17 | import UIKit 18 | #elseif canImport(AppKit) 19 | import AppKit 20 | #endif 21 | import VisualUtils 22 | 23 | public typealias VDot = Dot 24 | 25 | public final class Dot: VertexDebugger { 26 | 27 | public var position: CGPoint 28 | 29 | public lazy var vertex: Vertex = { 30 | createVertex(index: 0, position: position) 31 | }() 32 | 33 | public init( 34 | _ position: CGPoint, 35 | name: String? = nil, 36 | transform: Matrix2D = .identity, 37 | color: AppColor = .yellow, 38 | vertexShape: VertexShape = .shape(Circle(radius: 2)), 39 | labelStyle: TextRenderStyle = .nameLabel, 40 | useColorfulLabel: Bool = false 41 | ) { 42 | self.position = position 43 | super.init( 44 | name: name, 45 | transform: transform, 46 | color: color, 47 | vertexShape: vertexShape, 48 | labelStyle: labelStyle, 49 | useColorfulLable: useColorfulLabel 50 | ) 51 | } 52 | 53 | public func setStyle( 54 | shape: VertexShape? = nil, 55 | style: PathStyle? = nil, 56 | label: LabelStyle? = nil 57 | ) -> Dot { 58 | self.vertexStyleDict[0] = VertexStyle(shape: shape, style: style, label: label) 59 | return self 60 | } 61 | 62 | public func setStyle(_ style: VertexStyle) -> Dot { 63 | self.vertexStyleDict[0] = style 64 | return self 65 | } 66 | 67 | public func useColorfulLabel(_ value: Bool) -> Dot { 68 | self.useColorfulLabel = value 69 | return self 70 | } 71 | 72 | public func log(_ message: Any..., level: Logger.Log.Level = .info) -> Self { 73 | self.logging(message, level: level) 74 | return self 75 | } 76 | } 77 | 78 | extension Dot: DebugRenderable { 79 | public var debugBounds: CGRect? { 80 | let rect = CGRect(center: position, size: CGSize(width: 4, height: 4)) 81 | return rect * transform 82 | } 83 | 84 | public func render( 85 | with transform: Matrix2D, 86 | in context: CGContext, 87 | scale: CGFloat, 88 | contextHeight: Int? 89 | ) { 90 | vertex.render( 91 | with: self.transform * transform, 92 | in: context, 93 | scale: scale, 94 | contextHeight: contextHeight 95 | ) 96 | } 97 | } 98 | 99 | #Preview(traits: .fixedLayout(width: 400, height: 420)) { 100 | DebugView { 101 | Dot(.init(x: 150, y: 150), vertexShape: .shape(.circle(radius: 2))) 102 | .setStyle(style: .init(color: .green), label: "Hello") 103 | .applying(.init(translationX: 10, y: 10)) 104 | Dot(.init(x: 200, y: 100), color: .red, vertexShape: .index) 105 | } 106 | .coordinateVisible(true) 107 | .coordinateStyle(.default) 108 | .coordinateSystem(.yDown) 109 | } 110 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/ShapeRenderStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapeRenderStyle.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | 9 | import Foundation 10 | import CoreGraphics 11 | import SwiftUI 12 | import VisualUtils 13 | 14 | public protocol RenderableShape { 15 | var path: AppBezierPath { get } 16 | } 17 | 18 | public struct ShapeRenderStyle: Sendable { 19 | public struct Stroke: Sendable { 20 | public var color: AppColor 21 | public var style: StrokeStyle 22 | 23 | public init(color: AppColor, style: StrokeStyle) { 24 | self.color = color 25 | self.style = style 26 | } 27 | } 28 | public struct Fill: Sendable { 29 | public var color: AppColor 30 | public var style: FillStyle 31 | 32 | public init(color: AppColor, style: FillStyle = FillStyle()) { 33 | self.color = color 34 | self.style = style 35 | } 36 | } 37 | 38 | public var stroke: Stroke? 39 | public var fill: Fill? 40 | public var isEmpty: Bool { 41 | stroke == nil && fill == nil 42 | } 43 | 44 | public init(stroke: Stroke? = nil, fill: Fill? = nil) { 45 | self.stroke = stroke 46 | self.fill = fill 47 | } 48 | } 49 | 50 | extension ShapeRenderStyle.Stroke { 51 | public func set(for context: CGContext) { 52 | context.setStrokeColor(self.color.cgColor) 53 | context.setLineWidth(self.style.lineWidth) 54 | context.setLineCap(self.style.lineCap) 55 | context.setLineJoin(self.style.lineJoin) 56 | context.setMiterLimit(self.style.miterLimit) 57 | if !style.dash.isEmpty { 58 | context.setLineDash(phase: style.dashPhase, lengths: style.dash) 59 | } 60 | } 61 | } 62 | 63 | extension ShapeRenderStyle.Fill { 64 | public var rule: CGPathFillRule { 65 | style.isEOFilled ? .evenOdd : .winding 66 | } 67 | public func set(for context: CGContext) { 68 | context.setFillColor(self.color.cgColor) 69 | } 70 | } 71 | 72 | extension CGContext { 73 | 74 | public func render(shape: S, style: ShapeRenderStyle) where S: RenderableShape { 75 | let cgPath = shape.path.cgPath 76 | self.render(path: cgPath, style: style) 77 | } 78 | 79 | public func render(path cgPath: CGPath, style: ShapeRenderStyle) { 80 | guard !cgPath.isEmpty, !style.isEmpty else { return } 81 | self.saveGState() 82 | defer { self.restoreGState() } 83 | // fill 84 | if let fill = style.fill { 85 | self.addPath(cgPath) 86 | fill.set(for: self) 87 | self.fillPath(using: fill.rule) 88 | } 89 | 90 | // stroke 91 | if let stroke = style.stroke { 92 | self.addPath(cgPath) 93 | stroke.set(for: self) 94 | self.strokePath() 95 | } 96 | } 97 | 98 | public func renderCircle(center: CGPoint, radius: Double, style: ShapeRenderStyle) { 99 | let rect = CGRect(center: center, size: .init(width: radius * 2, height: radius * 2)) 100 | let path = AppBezierPath(ovalIn: rect) 101 | self.render(path: path.cgPath, style: style) 102 | } 103 | 104 | public func renderRect(_ rect: CGRect, style: ShapeRenderStyle) { 105 | let path = AppBezierPath(rect: rect) 106 | self.render(path: path.cgPath, style: style) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/algorithms/SegmentsCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SegmentsCollection.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/28. 6 | // 7 | 8 | 9 | import Foundation 10 | 11 | extension Collection { 12 | func segments(isClosed: Bool) -> SegmentsCollection { 13 | SegmentsCollection(base: self, isClosed: isClosed) 14 | } 15 | 16 | func closeSegments() -> SegmentsCollection { 17 | SegmentsCollection(base: self, isClosed: true) 18 | } 19 | 20 | func openSegments() -> SegmentsCollection { 21 | SegmentsCollection(base: self, isClosed: false) 22 | } 23 | } 24 | 25 | struct SegmentsCollection { 26 | internal let base: Base 27 | internal let isClosed: Bool 28 | 29 | internal init(base: Base, isClosed: Bool) { 30 | self.base = base 31 | self.isClosed = isClosed 32 | } 33 | } 34 | 35 | extension SegmentsCollection: Collection { 36 | typealias Element = (start: Base.Element, end: Base.Element) 37 | typealias Index = Base.Index 38 | 39 | var isEmpty: Bool { 40 | guard !base.isEmpty else { return true } 41 | if !isClosed { // open 42 | return base.count < 2 43 | } else { 44 | return base.isEmpty 45 | } 46 | } 47 | 48 | var count: Int { 49 | base.isEmpty ? 0 : base.count - 1 50 | } 51 | 52 | var startIndex: Index { 53 | return base.startIndex 54 | } 55 | 56 | var endIndex: Index { 57 | if isClosed { 58 | return base.endIndex 59 | } else { 60 | return base.index(base.endIndex, offsetBy: -1) 61 | } 62 | } 63 | 64 | var lastIndex: Index { 65 | return base.index(base.endIndex, offsetBy: -1) 66 | } 67 | 68 | subscript(position: Index) -> Element { 69 | let start = base[position] 70 | let end: Base.Element 71 | if position == lastIndex { 72 | end = base[base.startIndex] 73 | } else { 74 | end = base[base.index(after: position)] 75 | } 76 | return (start, end) 77 | } 78 | 79 | func index(after i: Index) -> Index { 80 | precondition(i != endIndex, "Advancing past end index") 81 | return base.index(after: i) 82 | } 83 | 84 | func makeIterator() -> SegmentsIterator { 85 | return SegmentsIterator(base: self) 86 | } 87 | } 88 | 89 | struct SegmentsIterator: IteratorProtocol { 90 | typealias Base = SegmentsCollection 91 | private var base: Base 92 | private var position: Base.Index 93 | 94 | init(base: Base) { 95 | self.base = base 96 | self.position = base.startIndex 97 | } 98 | 99 | mutating func next() -> Base.Element? { 100 | guard !self.base.isEmpty else { return nil } 101 | guard self.position >= base.startIndex, self.position < base.endIndex else { return nil } 102 | let result = base[position] 103 | self.position = base.index(after: position) 104 | return result 105 | } 106 | } 107 | 108 | extension SegmentsCollection: BidirectionalCollection where Base: BidirectionalCollection { 109 | func index(before i: Index) -> Index { 110 | precondition(i != startIndex, "Incrementing past start index") 111 | return base.index(before: i) 112 | } 113 | } 114 | 115 | extension SegmentsCollection: LazySequenceProtocol, LazyCollectionProtocol where Base: LazySequenceProtocol {} 116 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/segment/SegmentRenderElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SegmentElement.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/3. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public final class SegmentRenderElement: ContextRenderable { 12 | 13 | public let start: CGPoint 14 | public let end: CGPoint 15 | public let transform: Matrix2D 16 | public var startElement: PointElement? 17 | public var endElement: PointElement? 18 | public var centerElement: TextElement? 19 | public var offset: Double = 0 // rendering offset, 90 degree direction is positive 20 | public var startOffset: Double = 0 // shrink or expand start 21 | public var endOffset: Double = 0 // shrink or expand end 22 | public var segmentShape: SegmentRenderer? 23 | public var segmentStyle: ShapeRenderStyle 24 | 25 | var angle: Double { 26 | (end - start).angle 27 | } 28 | var center: CGPoint { 29 | (start + end) / 2.0 30 | } 31 | 32 | public init( 33 | start: CGPoint, 34 | end: CGPoint, 35 | transform: Matrix2D = .identity, 36 | segmentShape: SegmentRenderer?, 37 | segmentStyle: ShapeRenderStyle, 38 | startElement: PointElement? = nil, 39 | endElement: PointElement? = nil, 40 | centerElement: TextElement? = nil, 41 | offset: Double = 0, 42 | startOffset: Double = 0, 43 | endOffset: Double = 0 44 | ) { 45 | self.start = start 46 | self.end = end 47 | self.transform = transform 48 | self.segmentShape = segmentShape 49 | self.segmentStyle = segmentStyle 50 | self.startElement = startElement 51 | self.endElement = endElement 52 | self.centerElement = centerElement 53 | self.offset = offset 54 | self.startOffset = startOffset 55 | self.endOffset = endOffset 56 | } 57 | 58 | public func render(with matrix: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 59 | let transform = self.transform * matrix 60 | let s = start * transform 61 | let e = end * transform 62 | var seg = Segment(start: s, end: e) 63 | seg = seg.offseting(distance: offset) 64 | seg = seg.shrinkingStart(length: startOffset) 65 | seg = seg.shrinkingEnd(length: endOffset) 66 | 67 | let path: AppBezierPath 68 | if let segmentShape { 69 | path = segmentShape.getBezierPath(start: seg.start, end: seg.end) 70 | } else { 71 | path = AppBezierPath() 72 | path.move(to: seg.start) 73 | path.addLine(to: seg.end) 74 | } 75 | context.render(path: path.cgPath, style: segmentStyle) 76 | 77 | if let startElement { 78 | startElement.render( 79 | with: Matrix2D(translation: start) * transform, 80 | in: context, 81 | scale: scale, 82 | contextHeight: contextHeight 83 | ) 84 | } 85 | if let endElement { 86 | endElement.render( 87 | with: Matrix2D(translation: end) * transform, 88 | in: context, 89 | scale: scale, 90 | contextHeight: contextHeight 91 | ) 92 | } 93 | if let centerElement { 94 | let rotateM = Matrix2D(rotationAngle: seg.angle) 95 | let moveM = Matrix2D(translation: seg.center) 96 | centerElement.render( 97 | with: rotateM * moveM, 98 | in: context, 99 | scale: scale, 100 | contextHeight: contextHeight 101 | ) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/VisualUtils/CGPoint+Behavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | public extension Array where Element == CGPoint { 11 | var bounds: CGRect? { 12 | guard !self.isEmpty else { return nil } 13 | let pnt = self.first! 14 | var minX = pnt.x 15 | var minY = pnt.y 16 | var maxX = pnt.x 17 | var maxY = pnt.y 18 | 19 | for point in self { 20 | minX = point.x < minX ? point.x : minX 21 | minY = point.y < minY ? point.y : minY 22 | maxX = point.x > maxX ? point.x : maxX 23 | maxY = point.y > maxY ? point.y : maxY 24 | } 25 | 26 | return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) 27 | } 28 | } 29 | 30 | public extension CGPoint { 31 | var angle: CGFloat { 32 | return CGFloat(atan2(Double(self.y), Double(self.x))) 33 | } 34 | 35 | var length: CGFloat { 36 | return sqrt(self.x * self.x + self.y * self.y) 37 | } 38 | 39 | var squareLength: CGFloat { 40 | return self.x * self.x + self.y * self.y 41 | } 42 | } 43 | 44 | public func += (left: inout CGPoint, right: CGPoint) { 45 | left.x += right.x 46 | left.y += right.y 47 | } 48 | 49 | public func -= (left: inout CGPoint, right: CGPoint) { 50 | left.x -= right.x 51 | left.y -= right.y 52 | } 53 | 54 | public func - (left: CGPoint, right: CGPoint) -> CGPoint { 55 | return CGPoint(x: left.x - right.x, y: left.y - right.y) 56 | } 57 | 58 | public func + (left: CGPoint, right: CGPoint) -> CGPoint { 59 | return CGPoint(x: left.x + right.x, y: left.y + right.y) 60 | } 61 | 62 | public prefix func - (point: CGPoint) -> CGPoint { 63 | return CGPoint(x: -point.x, y: -point.y) 64 | } 65 | 66 | public func / (left: CGPoint, right: CGFloat) -> CGPoint { 67 | return CGPoint(x: left.x / right, y: left.y / right) 68 | } 69 | 70 | public func * (left: CGPoint, right: CGFloat) -> CGPoint { 71 | return CGPoint(x: left.x * right, y: left.y * right) 72 | } 73 | 74 | public func * (left: CGFloat, right: CGPoint) -> CGPoint { 75 | return CGPoint(x: left * right.x, y: left * right.y) 76 | } 77 | 78 | public func * (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 79 | return CGPoint(x: lhs.x * rhs.x, y: lhs.y * rhs.y) 80 | } 81 | 82 | public func *= (lhs: inout CGPoint, rhs: CGFloat) { 83 | lhs.x *= rhs 84 | lhs.y *= rhs 85 | } 86 | 87 | public func /= (lhs: inout CGPoint, rhs: CGFloat) { 88 | lhs.x /= rhs 89 | lhs.y /= rhs 90 | } 91 | 92 | public extension Array where Element == CGPoint { 93 | var gravityCenter: CGPoint { 94 | var c = CGPoint() 95 | var area: CGFloat = 0.0 96 | let p1X: CGFloat = 0.0 97 | let p1Y: CGFloat = 0.0 98 | let inv3: CGFloat = 1.0 / 3.0 99 | 100 | for i in stride(from: self.startIndex, to: self.endIndex, by: 1) { 101 | let p2 = self[i] 102 | let next = self.index(i, offsetBy: 1) 103 | let p3 = (next == self.endIndex ? self[self.startIndex] : self[next]) 104 | 105 | let e1X = p2.x - p1X 106 | let e1Y = p2.y - p1Y 107 | let e2X = p3.x - p1X 108 | let e2Y = p3.y - p1Y 109 | 110 | let D = (e1X * e2Y - e1Y * e2X) 111 | 112 | let triangleArea = 0.5 * D 113 | area += triangleArea 114 | 115 | c.x += triangleArea * inv3 * (p1X + p2.x + p3.x) 116 | c.y += triangleArea * inv3 * (p1Y + p2.y + p3.y) 117 | } 118 | 119 | c.x *= 1.0 / area 120 | c.y *= 1.0 / area 121 | 122 | return c 123 | } 124 | 125 | var polyIsCCW: Bool { 126 | var sum: CGFloat = 0.0 127 | for i in 0.. 0 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/DebugCapture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugCapture.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/6. 6 | // 7 | 8 | import Foundation 9 | #if canImport(UIKit) 10 | import UIKit 11 | #elseif canImport(AppKit) 12 | import AppKit 13 | #endif 14 | import VisualUtils 15 | 16 | public final class DebugCapture: @unchecked Sendable { 17 | 18 | public let context: DebugContext 19 | public let scale: Double 20 | public let folder: URL 21 | 22 | private var imageCaches: [AppImage] = [] 23 | 24 | public init(context: DebugContext, folder: URL, scale: Double = 2) { 25 | self.context = context 26 | self.scale = scale 27 | self.folder = folder 28 | } 29 | 30 | public func captureObjects( 31 | _ action: String? = nil, 32 | @DebugBuilder _ builder: () -> [any Debuggable] 33 | ) { 34 | var elements = builder().map{ $0.debugElements }.flatMap{ $0 } 35 | if let action { 36 | let textElement = TextElement(source: .string(action), style: .nameLabel) 37 | let text = StaticTextElement(content: textElement, position: context.coordinate.valueRect.bottomCenter) 38 | elements.append(text) 39 | } 40 | 41 | if let image = context.getImage(scale: scale, elements: elements) { 42 | imageCaches.append(image) 43 | } 44 | } 45 | 46 | public func captureElements( 47 | _ action: String? = nil, 48 | @RenderBuilder _ builder: () -> [any ContextRenderable] 49 | ) { 50 | var elements = builder() 51 | if let action { 52 | let textElement = TextElement(source: .string(action), style: .nameLabel) 53 | let text = StaticTextElement(content: textElement, position: context.coordinate.valueRect.bottomCenter) 54 | elements.append(text) 55 | } 56 | 57 | if let image = context.getImage(scale: scale, elements: elements) { 58 | imageCaches.append(image) 59 | } 60 | } 61 | 62 | public func output() { 63 | do { 64 | try FileManager.default.createDirectory( 65 | at: folder, 66 | withIntermediateDirectories: true, 67 | attributes: nil 68 | ) 69 | } catch { 70 | print("Failed to create directory: \(error)") 71 | return 72 | } 73 | for (i, image) in imageCaches.enumerated() { 74 | let fileName = String(format: "%05d.png", i + 1) 75 | let fileURL = folder.appendingPathComponent(fileName) 76 | do { 77 | #if os(iOS) 78 | var imageData = image.pngData() 79 | #elseif os(macOS) 80 | var imageData: Data? 81 | if let tiffData = image.tiffRepresentation, 82 | let bitmapRep = NSBitmapImageRep(data: tiffData), 83 | let data = bitmapRep.representation(using: .png, properties: [:]) { 84 | imageData = data 85 | } 86 | #endif 87 | if let imageData { 88 | try imageData.write(to: fileURL) 89 | } else { 90 | print("Failed to convert image at index \(i) to PNG data") 91 | } 92 | } catch { 93 | print("Failed to save image \(fileName): \(error)") 94 | } 95 | } 96 | imageCaches.removeAll() 97 | } 98 | } 99 | 100 | extension DebugCapture { 101 | private static let lock = NSLock() 102 | nonisolated(unsafe) private static var _shared: DebugCapture? 103 | public static var shared: DebugCapture? { 104 | get { 105 | lock.lock() 106 | let value = _shared 107 | lock.unlock() 108 | return value 109 | } 110 | set { 111 | lock.lock() 112 | _shared = newValue 113 | lock.unlock() 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/coordinateSystem/AxisRenderElement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XAxis.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/4. 6 | // 7 | 8 | import CoreGraphics 9 | #if canImport(UIKit) 10 | import UIKit 11 | #elseif canImport(AppKit) 12 | import AppKit 13 | #endif 14 | import VisualUtils 15 | 16 | extension Int { 17 | @usableFromInline 18 | static let markPrecision: Int = 6 19 | } 20 | 21 | final class AxisRenderElement: ContextRenderable { 22 | static let markLength: Double = 6 23 | 24 | let axis: Axis 25 | let color: AppColor 26 | let coord: CoordinateSystem2D 27 | 28 | lazy var markStyle = ShapeRenderStyle( 29 | stroke: .init(color: color, style: .init(lineWidth: 1)) 30 | ) 31 | lazy var arrowStyle = ShapeRenderStyle( 32 | stroke: .init(color: color, style: .init(lineWidth: 1)), 33 | fill: nil 34 | ) 35 | lazy var labelStyle = TextRenderStyle( 36 | font: AppFont(name: "HelveticaNeueInterface-Thin", size: 10) ?? AppFont.systemFont(ofSize: 10), 37 | insets: .zero, 38 | margin: AppEdgeInsets(top: 2, left: 2, bottom: 2, right: 2), 39 | anchor: .topCenter, 40 | textColor: color 41 | ) 42 | 43 | lazy var marks = createMarks() 44 | lazy var arrow = axis.getElement(style: arrowStyle) 45 | 46 | init( 47 | axis: Axis, 48 | color: AppColor = .lightGray, 49 | coord: CoordinateSystem2D 50 | ) { 51 | self.axis = axis 52 | self.color = color 53 | self.coord = coord 54 | } 55 | 56 | func render( 57 | with transform: Matrix2D, 58 | in context: CGContext, 59 | scale: CGFloat, 60 | contextHeight: Int? 61 | ) { 62 | for mark in marks { 63 | mark.render(with: transform, in: context, scale: scale, contextHeight: contextHeight) 64 | } 65 | self.arrow.render(with: transform, in: context, scale: scale, contextHeight: contextHeight) 66 | } 67 | 68 | func createMarks() -> [StaticRenderElement] { 69 | return axis.marks.compactMap { mark in 70 | if mark.position == axis.origin.position { return nil } 71 | return mark.getElement( 72 | length: Self.markLength, 73 | precision: .markPrecision, 74 | coord: coord, 75 | markStyle: markStyle, 76 | labelStyle: labelStyle 77 | ) 78 | } 79 | } 80 | } 81 | 82 | extension Axis.Mark { 83 | 84 | func estimateSize() -> CGSize { 85 | let text = TextSource.number(value: self.value, precision: .markPrecision).string 86 | return TextRenderStyle.xAxisLabel.getTextSize(text: text) 87 | } 88 | 89 | func getElement( 90 | length: Double, 91 | precision: Int, 92 | coord: CoordinateSystem2D, 93 | markStyle: ShapeRenderStyle, 94 | labelStyle: TextRenderStyle 95 | ) -> StaticRenderElement { 96 | var textStyle = labelStyle 97 | let path = AppBezierPath() 98 | path.move(to: .zero) 99 | switch type { 100 | case .x: 101 | switch coord { 102 | case .yUp: 103 | path.addLine(to: .init(x: 0, y: -length)) 104 | textStyle.setTextLocation(.bottom) 105 | case .yDown: 106 | path.addLine(to: .init(x: 0, y: length)) 107 | textStyle.setTextLocation(.top) 108 | } 109 | case .y: 110 | path.addLine(to: .init(x: length, y: 0)) 111 | textStyle.setTextLocation(.left) 112 | } 113 | 114 | let shape = ShapeElement(renderer: path, style: markStyle) 115 | let label = TextElement(source: .number(value: self.value, precision: precision), style: textStyle) 116 | let point = PointElement(shape: shape, label: label) 117 | return .init(content: point, position: position) 118 | } 119 | } 120 | 121 | extension Axis { 122 | func getElement(style: ShapeRenderStyle) -> SegmentRenderElement { 123 | return SegmentRenderElement( 124 | start: start.position, 125 | end: end.position, 126 | segmentShape: Arrow(tip: .init(anchor: .midLeft)), 127 | segmentStyle: style 128 | ) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/VisualUtils/Segment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Segment.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/27. 6 | // 7 | 8 | import CoreGraphics 9 | 10 | // TODO: using center, angle, length to avoid zero length offset issue 11 | public struct Segment { 12 | public var start: CGPoint 13 | public var end: CGPoint 14 | 15 | public init(start: CGPoint, end: CGPoint) { 16 | self.start = start 17 | self.end = end 18 | } 19 | 20 | // 计算从 start 到 end 的向量 21 | private var vector: CGPoint { 22 | return CGPoint(x: end.x - start.x, y: end.y - start.y) 23 | } 24 | 25 | // 计算线段长度 26 | public var length: Double { 27 | let v = vector 28 | return sqrt(v.x * v.x + v.y * v.y) 29 | } 30 | 31 | public var angle: Double { 32 | vector.angle 33 | } 34 | 35 | public var center: CGPoint { 36 | (start + end) / 2.0 37 | } 38 | 39 | // 返回单位向量 40 | private func unitVector() -> CGPoint { 41 | let L = length 42 | return L > 0 ? CGPoint(x: vector.x / L, y: vector.y / L) : .zero 43 | } 44 | 45 | // 辅助方法:沿指定向量移动点 46 | private func movePoint(_ point: CGPoint, by distance: Double, along vector: CGPoint) -> CGPoint { 47 | var newPoint = point 48 | newPoint.x += distance * vector.x 49 | newPoint.y += distance * vector.y 50 | return newPoint 51 | } 52 | 53 | // 从两端收缩线段 54 | public func shrinking(length: Double) -> Segment { 55 | if length <= 0 || self.length == 0 { return self } 56 | let d = min(length, self.length / 2) 57 | let uv = unitVector() 58 | let newStart = movePoint(start, by: d, along: uv) 59 | let newEnd = movePoint(end, by: -d, along: uv) 60 | return Segment(start: newStart, end: newEnd) 61 | } 62 | 63 | // 从起点收缩线段 64 | public func shrinkingStart(length: Double) -> Segment { 65 | if length <= 0 || self.length == 0 { return self } 66 | let d = min(length, self.length) 67 | let uv = unitVector() 68 | let newStart = movePoint(start, by: d, along: uv) 69 | return Segment(start: newStart, end: end) 70 | } 71 | 72 | // 从终点收缩线段 73 | public func shrinkingEnd(length: Double) -> Segment { 74 | if length <= 0 || self.length == 0 { return self } 75 | let d = min(length, self.length) 76 | let uv = unitVector() 77 | let newEnd = movePoint(end, by: -d, along: uv) 78 | return Segment(start: start, end: newEnd) 79 | } 80 | 81 | // 向两端扩展线段 82 | public func expanding(length: Double) -> Segment { 83 | if length <= 0 { return self } 84 | let uv = unitVector() 85 | let newStart = movePoint(start, by: -length, along: uv) 86 | let newEnd = movePoint(end, by: length, along: uv) 87 | return Segment(start: newStart, end: newEnd) 88 | } 89 | 90 | // 从起点扩展线段 91 | public func expandingStart(length: Double) -> Segment { 92 | if length <= 0 { return self } 93 | let uv = unitVector() 94 | let newStart = movePoint(start, by: -length, along: uv) 95 | return Segment(start: newStart, end: end) 96 | } 97 | 98 | // 从终点扩展线段 99 | public func expandingEnd(length: Double) -> Segment { 100 | if length <= 0 { return self } 101 | let uv = unitVector() 102 | let newEnd = movePoint(end, by: length, along: uv) 103 | return Segment(start: start, end: newEnd) 104 | } 105 | 106 | public func offseting(distance: Double) -> Segment { 107 | // If distance is 0 or segment has no length, return original segment 108 | if distance == 0 || self.length == 0 { return self } 109 | 110 | // Get unit vector of the segment 111 | let uv = unitVector() 112 | 113 | // Rotate 90 degrees counterclockwise (positive direction) 114 | // For a vector (x, y), 90° rotation CCW is (-y, x) 115 | let perpendicular = CGPoint(x: -uv.y, y: uv.x) 116 | 117 | // Move both start and end points along perpendicular vector 118 | let newStart = movePoint(start, by: distance, along: perpendicular) 119 | let newEnd = movePoint(end, by: distance, along: perpendicular) 120 | 121 | return Segment(start: newStart, end: newEnd) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logs.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/22. 6 | // 7 | 8 | import Foundation 9 | import CoreGraphics 10 | import VisualUtils 11 | 12 | public typealias VLogger = Logger 13 | 14 | public final class Logger: @unchecked Sendable { 15 | // 单例模式 16 | public static let `default`: Logger = .init() 17 | 18 | // 日志结构体 19 | public struct Log { 20 | public enum Level { 21 | case debug 22 | case info 23 | case warning 24 | case error 25 | case critical 26 | 27 | // 返回对应颜色 28 | var color: AppColor { 29 | switch self { 30 | case .debug: 31 | return .gray 32 | case .info: 33 | return .green 34 | case .warning: 35 | return .yellow 36 | case .error: 37 | return .red 38 | case .critical: 39 | return .purple 40 | } 41 | } 42 | 43 | // 返回显示名称 44 | var displayName: String { 45 | switch self { 46 | case .debug: return "DEBUG" 47 | case .info: return "INFO" 48 | case .warning: return "WARNING" 49 | case .error: return "ERROR" 50 | case .critical: return "CRITICAL" 51 | } 52 | } 53 | } 54 | 55 | public var message: String 56 | public var level: Level 57 | } 58 | 59 | // 线程安全队列 60 | private let queue = DispatchQueue(label: "logger.queue") 61 | public private(set) var logs: [Log] = [] 62 | 63 | // 私有初始化,确保单例 64 | private init() {} 65 | 66 | // 核心日志记录方法 67 | func logging(_ messages: [Any], level: Logger.Log.Level = .info, separator: String = ", ") { 68 | let stringMessage = messages.map { String(reflecting: $0) }.joined(separator: separator) 69 | let log = Logger.Log(message: stringMessage, level: level) 70 | queue.sync { 71 | logs.append(log) 72 | // 打印到控制台,包含时间戳和级别 73 | //let timestamp = ISO8601DateFormatter().string(from: Date()) 74 | //print("[\(timestamp)] [\(level.displayName)] \(message)") 75 | } 76 | } 77 | 78 | // 便利方法,按经典命名标准实现 79 | public func debug(_ message: Any...) { 80 | logging(message, level: .debug) 81 | } 82 | 83 | public func info(_ message: Any...) { 84 | logging(message, level: .info) 85 | } 86 | 87 | public func warning(_ message: Any...) { 88 | logging(message, level: .warning) 89 | } 90 | 91 | public func error(_ message: Any...) { 92 | logging(message, level: .error) 93 | } 94 | 95 | public func critical(_ message: Any...) { 96 | logging(message, level: .critical) 97 | } 98 | 99 | // 可选:获取所有日志的方法 100 | public func getLogs() -> [Log] { 101 | queue.sync { 102 | return logs 103 | } 104 | } 105 | } 106 | 107 | extension TextRenderStyle { 108 | public static func log(color: AppColor) -> TextRenderStyle { 109 | TextRenderStyle( 110 | font: AppFont.italicSystemFont(ofSize: 10), 111 | insets: .zero, 112 | margin: AppEdgeInsets(top: 2, left: 10, bottom: 2, right: 2), 113 | anchor: .topLeft, 114 | textStroke: .init(color: .black, width: -30), 115 | textColor: color 116 | ) 117 | } 118 | } 119 | 120 | extension Logger.Log { 121 | func textElement(at position: CGPoint) -> StaticTextElement? { 122 | guard !message.isEmpty else { return nil } 123 | return StaticTextElement(source: .string(message), style: .log(color: self.level.color), position: position) 124 | } 125 | } 126 | 127 | extension Array where Element == Logger.Log { 128 | public func render( 129 | in context: CGContext, 130 | scale: CGFloat, 131 | contextHeight: Int? 132 | ) { 133 | var y: CGFloat = 25 134 | let lineHeight: CGFloat = 12 135 | for log in self { 136 | let element = log.textElement(at: CGPoint(x: 0, y: y)) 137 | element?.render(with: .identity, in: context, scale: scale, contextHeight: contextHeight) 138 | y += lineHeight 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/base/GeometryDebugger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseDebugger.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/5. 6 | // 7 | #if canImport(UIKit) 8 | import UIKit 9 | #elseif canImport(AppKit) 10 | import AppKit 11 | #endif 12 | import VisualUtils 13 | 14 | public class GeometryDebugger: SegmentDebugger { 15 | 16 | public let faceStyle: FaceStyle 17 | public var faceStyleDict: [Int: FaceStyle] = [:] 18 | 19 | public init( 20 | name: String? = nil, 21 | transform: Matrix2D = .identity, 22 | vertexShape: VertexShape = .shape(Circle(radius: 2)), 23 | edgeShape: EdgeShape = .arrow(Arrow()), 24 | color: AppColor = .yellow, 25 | vertexStyleDict: [Int: VertexStyle] = [:], 26 | edgeStyleDict: [Int: EdgeStyle] = [:], 27 | faceStyleDict: [Int: FaceStyle] = [:], 28 | displayOptions: DisplayOptions = .all, 29 | labelStyle: TextRenderStyle = .nameLabel, 30 | useColorfulLable: Bool = false 31 | ) { 32 | self.faceStyle = FaceStyle(style: .init(color: color.withAlphaComponent(0.2)), label: nil) 33 | self.faceStyleDict = faceStyleDict 34 | super.init( 35 | name: name, 36 | transform: transform, 37 | color: color, 38 | vertexShape: vertexShape, 39 | edgeShape: edgeShape, 40 | displayOptions: displayOptions, 41 | labelStyle: labelStyle, 42 | useColorfulLable: useColorfulLable, 43 | vertexStyleDict: vertexStyleDict, 44 | edgeStyleDict: edgeStyleDict 45 | ) 46 | } 47 | 48 | func getFaceRenderStyle(style: PathStyle?) -> ShapeRenderStyle { 49 | let color = style?.color ?? color.withAlphaComponent(0.2) 50 | guard let mode = style?.mode else { 51 | return ShapeRenderStyle( 52 | stroke: nil, 53 | fill: .init(color: color) 54 | ) 55 | } 56 | switch mode { 57 | case .stroke(dashed: let dashed): 58 | let dash: [CGFloat] = dashed ? [5, 5] : [] 59 | return ShapeRenderStyle( 60 | stroke: .init(color: color, style: .init(lineWidth: 1, dash: dash)), 61 | fill: nil 62 | ) 63 | case .fill: 64 | return ShapeRenderStyle( 65 | stroke: nil, 66 | fill: .init(color: color, style: .init()) 67 | ) 68 | } 69 | } 70 | 71 | func createFace( 72 | vertices: [CGPoint], 73 | faceIndex: Int 74 | ) -> FaceRenderElement { 75 | let customStyle = faceStyleDict[faceIndex] 76 | 77 | var labelString: String? 78 | if let faceLabel = customStyle?.label?.text { 79 | switch faceLabel { 80 | case .string(let string): 81 | labelString = string 82 | case .coordinate: 83 | labelString = "\(vertices.gravityCenter)" 84 | case .index: 85 | labelString = "\(faceIndex)" 86 | case .orientation: 87 | labelString = vertices.polyIsCCW ? "↺" : "↻" 88 | } 89 | } 90 | let textColor: AppColor? = if useColorfulLabel { customStyle?.style?.color ?? self.color } else { nil } 91 | var label: TextElement? 92 | if let labelString { 93 | if let labelStyle = customStyle?.label?.style { 94 | label = TextElement(text: labelString, style: labelStyle) 95 | } else { 96 | label = TextElement( 97 | text: labelString, 98 | defaultStyle: labelStyle, 99 | location: customStyle?.label?.location ?? .center, 100 | textColor: textColor 101 | ) 102 | } 103 | } 104 | return FaceRenderElement( 105 | points: vertices, 106 | transform: transform, 107 | style: getFaceRenderStyle(style: customStyle?.style), 108 | label: label 109 | ) 110 | } 111 | } 112 | 113 | extension GeometryDebugger { 114 | public typealias MeshFace = FaceRenderElement 115 | 116 | public struct FaceStyle { 117 | let style: PathStyle? 118 | let label: LabelStyle? 119 | 120 | public init(style: PathStyle?, label: LabelStyle?) { 121 | self.style = style 122 | self.label = label 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/VisualUtils/Alias.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Alias.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | public typealias AppBezierPath = UIBezierPath 12 | public typealias AppPasteboard = UIPasteboard 13 | public typealias AppScreen = UIScreen 14 | public typealias AppColor = UIColor 15 | public typealias AppImage = UIImage 16 | public typealias AppFont = UIFont 17 | public typealias AppEdgeInsets = UIEdgeInsets 18 | public typealias AppKeyModifierFlags = UIKeyModifierFlags 19 | public typealias AppEvent = UIEvent 20 | public typealias AppView = UIView 21 | 22 | #elseif canImport(AppKit) && !targetEnvironment(macCatalyst) 23 | import AppKit 24 | public typealias AppBezierPath = NSBezierPath 25 | public typealias AppPasteboard = NSPasteboard 26 | public typealias AppScreen = NSScreen 27 | public typealias AppColor = NSColor 28 | public typealias AppImage = NSImage 29 | public typealias AppFont = NSFont 30 | public typealias AppEdgeInsets = NSEdgeInsets 31 | public typealias AppKeyModifierFlags = NSEvent.ModifierFlags 32 | public typealias AppEvent = NSEvent 33 | public typealias AppView = NSView 34 | 35 | extension NSEdgeInsets { 36 | public static let zero = NSEdgeInsets() 37 | } 38 | 39 | extension NSFont { 40 | public static func italicSystemFont(ofSize size: CGFloat) -> NSFont { 41 | let baseFont = NSFont.systemFont(ofSize: size) 42 | let italicDescriptor = baseFont.fontDescriptor.withSymbolicTraits(.italic) 43 | // Create the italic font using the descriptor 44 | return NSFont(descriptor: italicDescriptor, size: size) ?? baseFont 45 | } 46 | } 47 | 48 | public extension NSBezierPath { 49 | 50 | convenience init(cgPath: CGPath) { 51 | self.init() 52 | cgPath.applyWithBlock { pointer in 53 | let element = pointer.pointee 54 | let points = element.points 55 | switch element.type { 56 | case .moveToPoint: 57 | self.move(to: points[0]) 58 | case .addLineToPoint: 59 | self.line(to: points[0]) 60 | case .addQuadCurveToPoint: 61 | addQuadCurve(to: points[1], controlPoint: points[0]) 62 | case .addCurveToPoint: 63 | curve(to: points[2], controlPoint1: points[0], controlPoint2: points[1]) 64 | case .closeSubpath: 65 | close() 66 | default: break 67 | } 68 | } 69 | } 70 | 71 | convenience init(roundedRect: CGRect, cornerRadius: CGFloat) { 72 | self.init(roundedRect: roundedRect, xRadius: cornerRadius, yRadius: cornerRadius) 73 | } 74 | 75 | func apply(_ t: CGAffineTransform) { 76 | let transform = AffineTransform( 77 | m11: t.a, 78 | m12: t.b, 79 | m21: t.c, 80 | m22: t.d, 81 | tX: t.tx, 82 | tY: t.ty 83 | ) 84 | self.transform(using: transform) 85 | } 86 | 87 | func reversing() -> NSBezierPath { 88 | return self.reversed 89 | } 90 | 91 | func addLine(to point: CGPoint) { 92 | self.line(to: point) 93 | } 94 | 95 | func addCurve( 96 | to point: CGPoint, 97 | controlPoint1 point1: CGPoint, 98 | controlPoint2 point2: CGPoint 99 | ) { 100 | self.curve(to: point, controlPoint1: point1, controlPoint2: point2) 101 | } 102 | 103 | private func interpolate(_ p1: CGPoint, _ p2: CGPoint, _ ratio: CGFloat) -> CGPoint { 104 | return CGPoint(x: p1.x + (p2.x - p1.x) * ratio, y: p1.y + (p2.y - p1.y) * ratio) 105 | } 106 | 107 | func addQuadCurve(to end: CGPoint, controlPoint: CGPoint) { 108 | let start = self.currentPoint 109 | let control1 = self.interpolate(start, controlPoint, 0.666666) 110 | let control2 = self.interpolate(end, controlPoint, 0.666666) 111 | self.curve(to: end, controlPoint1: control1, controlPoint2: control2) 112 | } 113 | 114 | func addArc( 115 | withCenter center: CGPoint, 116 | radius: CGFloat, 117 | startAngle: CGFloat, 118 | endAngle: CGFloat, 119 | clockwise: Bool 120 | ) { 121 | self.appendArc( 122 | withCenter: center, 123 | radius: radius, 124 | startAngle: startAngle * 180 / .pi, 125 | endAngle: endAngle * 180 / .pi, 126 | clockwise: !clockwise 127 | ) 128 | } 129 | } 130 | #endif 131 | 132 | import CoreGraphics 133 | public typealias Matrix2D = CGAffineTransform 134 | 135 | -------------------------------------------------------------------------------- /VisualDebugger.xcworkspace/Playground.playground/Pages/TestMorePoints.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import UIKit 5 | import VisualDebugger 6 | 7 | let points2:[CGPoint] = [CGPoint(x: 102.920059204102, y: 309.248260498047),CGPoint(x: 106.614501953125, y: 344.716369628906),CGPoint(x: 113.737640380859, y: 374.837127685547),CGPoint(x: 120.512191772461, y: 401.394378662109),CGPoint(x: 127.473999023438, y: 427.603668212891),CGPoint(x: 143.475402832031, y: 453.217620849609),CGPoint(x: 169.710861206055, y: 476.416351318359),CGPoint(x: 195.409881591797, y: 495.858093261719),CGPoint(x: 231.770385742188, y: 498.309906005859),CGPoint(x: 270.018127441406, y: 492.324645996094),CGPoint(x: 295.751281738281, y: 471.697906494141),CGPoint(x: 324.231628417969, y: 445.360809326172),CGPoint(x: 338.780578613281, y: 416.582489013672),CGPoint(x: 345.022399902344, y: 385.388214111328),CGPoint(x: 347.763641357422, y: 357.915588378906),CGPoint(x: 348.241485595703, y: 326.882354736328),CGPoint(x: 344.511810302734, y: 291.121002197266),CGPoint(x: 112.766448974609, y: 263.260864257812),CGPoint(x: 126.955825805664, y: 246.219604492188),CGPoint(x: 147.221054077148, y: 240.242141723633),CGPoint(x: 167.287582397461, y: 243.987640380859),CGPoint(x: 188.532241821289, y: 250.977813720703),CGPoint(x: 230.694122314453, y: 245.525924682617),CGPoint(x: 252.492858886719, y: 233.592361450195),CGPoint(x: 273.958862304688, y: 226.215255737305),CGPoint(x: 296.547729492188, y: 226.177947998047),CGPoint(x: 314.780639648438, y: 237.954956054688),CGPoint(x: 212.333099365234, y: 274.396820068359),CGPoint(x: 213.848236083984, y: 291.7666015625),CGPoint(x: 215.682159423828, y: 309.381774902344),CGPoint(x: 217.270431518555, y: 327.306030273438),CGPoint(x: 197.25830078125, y: 360.563079833984),CGPoint(x: 210.22998046875, y: 361.284240722656),CGPoint(x: 223.292907714844, y: 360.623962402344),CGPoint(x: 236.039138793945, y: 357.434173583984),CGPoint(x: 249.03157043457, y: 353.239837646484),CGPoint(x: 136.467178344727, y: 288.504425048828),CGPoint(x: 149.303848266602, y: 279.018371582031),CGPoint(x: 163.200759887695, y: 278.1962890625),CGPoint(x: 179.751098632812, y: 288.321594238281),CGPoint(x: 163.361389160156, y: 294.072418212891),CGPoint(x: 148.922424316406, y: 294.649963378906),CGPoint(x: 251.513473510742, y: 279.21875),CGPoint(x: 266.404846191406, y: 266.611419677734),CGPoint(x: 281.165161132812, y: 264.357818603516),CGPoint(x: 294.942687988281, y: 271.381072998047),CGPoint(x: 282.074096679688, y: 280.328765869141),CGPoint(x: 266.892730712891, y: 282.081604003906),CGPoint(x: 183.944412231445, y: 420.126556396484),CGPoint(x: 195.461242675781, y: 395.975433349609),CGPoint(x: 211.55973815918, y: 383.063720703125),CGPoint(x: 226.484359741211, y: 385.321899414062),CGPoint(x: 240.440826416016, y: 380.547302246094),CGPoint(x: 259.970642089844, y: 390.708404541016),CGPoint(x: 276.713806152344, y: 413.516510009766),CGPoint(x: 263.826965332031, y: 429.364318847656),CGPoint(x: 247.461242675781, y: 439.414978027344),CGPoint(x: 230.004486083984, y: 443.536865234375),CGPoint(x: 211.801132202148, y: 442.48583984375),CGPoint(x: 197.189666748047, y: 435.528259277344),CGPoint(x: 211.676773071289, y: 398.491851806641),CGPoint(x: 226.799194335938, y: 396.774993896484),CGPoint(x: 241.258483886719, y: 396.060699462891),CGPoint(x: 243.191757202148, y: 426.095947265625),CGPoint(x: 228.474853515625, y: 429.870239257812),CGPoint(x: 213.509262084961, y: 428.634185791016),CGPoint(x: 181.226928710938, y: 346.06982421875),CGPoint(x: 186.374954223633, y: 327.718719482422),CGPoint(x: 252.590042114258, y: 322.635192871094),CGPoint(x: 260.98291015625, y: 340.490936279297),CGPoint(x: 105.330871582031, y: 282.951019287109),CGPoint(x: 114.405067443848, y: 229.309616088867),CGPoint(x: 156.571624755859, y: 176.383010864258),CGPoint(x: 209.813125610352, y: 159.575103759766),CGPoint(x: 267.726806640625, y: 167.542587280273),CGPoint(x: 320.730682373047, y: 213.157363891602),CGPoint(x: 339.590850830078, y: 264.923828125),CGPoint(x: 211.097229003906, y: 206.810882568359), CGPoint(x: 160, y: 160), CGPoint(x: 400, y: 400), CGPoint(x: 200, y: 200)] 8 | 9 | let points3:[CGPoint] = [CGPoint(x: 0.00143313372973353, y: -0.00193239329382777),CGPoint(x: 0.00143621955066919, y: -0.00193460355512798),CGPoint(x: 0.00143919070251286, y: -0.00193574069999158),CGPoint(x: 0.00144259433727711, y: -0.00193591101560742),CGPoint(x: 0.0014466285938397, y: -0.00193509564269334),CGPoint(x: 0.00144996808376163, y: -0.0019329950446263),CGPoint(x: 0.00145198323298246, y: -0.00193005020264536),CGPoint(x: 0.00144895480480045, y: -0.0019282060675323),CGPoint(x: 0.00144564209040254, y: -0.00192755076568574),CGPoint(x: 0.00144167093094438, y: -0.00192762282676995),CGPoint(x: 0.00143746938556433, y: -0.00192866427823901),CGPoint(x: 0.00143489660695195, y: -0.00192986987531185)] 10 | 11 | 12 | //points2.debugView 13 | //let pnts = Points.init(points: points2, representation: .indices) 14 | //debug([pnts, points2, points2[10]], visibleRect: CGRect(x: 0, y: 0, width: 480, height: 640)) 15 | 16 | debug(points2) 17 | -------------------------------------------------------------------------------- /merge_code.command: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | from datetime import datetime 5 | 6 | def merge_files(source_dirs, output_file, file_types): 7 | """ 8 | 合并多个指定目录下的指定类型文件到一个输出文件 9 | 10 | Args: 11 | source_dirs (list): 源代码目录路径的列表 12 | output_file (str): 输出文件的路径 13 | file_types (list): 要合并的文件类型列表,如 ['.c', '.h'] 14 | """ 15 | try: 16 | # 检查是否提供了源目录 17 | if not source_dirs: 18 | raise ValueError("没有提供源目录") 19 | 20 | # 验证每个源目录并收集目标文件 21 | target_files = [] 22 | valid_dirs = [] 23 | for source_dir in source_dirs: 24 | if not os.path.exists(source_dir): 25 | print(f"警告: 目录不存在: {source_dir},将跳过此目录") 26 | continue 27 | valid_dirs.append(source_dir) 28 | for root, _, files in os.walk(source_dir): 29 | for file in files: 30 | if any(file.endswith(ext) for ext in file_types): 31 | target_files.append((source_dir, os.path.join(root, file))) 32 | 33 | # 如果没有找到任何文件 34 | if not target_files: 35 | print(f"警告: 未在提供的目录中找到以下类型的文件: {', '.join(file_types)}") 36 | return False 37 | 38 | # 创建输出文件 39 | with open(output_file, 'w', encoding='utf-8') as outfile: 40 | # 写入文件头部信息 41 | outfile.write(f"// 合并的源代码文件\n") 42 | outfile.write(f"// 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") 43 | outfile.write(f"// 源目录: {', '.join(valid_dirs)}\n") 44 | outfile.write(f"// 文件类型: {', '.join(file_types)}\n\n") 45 | 46 | # 合并所有文件 47 | for source_dir, file_path in sorted(target_files, key=lambda x: x[1]): 48 | try: 49 | with open(file_path, 'r', encoding='utf-8') as infile: 50 | content = infile.read() 51 | 52 | # 计算相对路径并写入文件分隔符 53 | rel_path = os.path.relpath(file_path, source_dir) 54 | outfile.write(f"\n// ========================= From {source_dir}: {rel_path} =========================\n\n") 55 | outfile.write(content) 56 | outfile.write("\n") 57 | print(f"已处理: {file_path}") 58 | 59 | except Exception as e: 60 | print(f"警告: 处理文件 {file_path} 时出错: {str(e)}") 61 | continue 62 | 63 | print(f"\n成功合并了 {len(target_files)} 个文件到: {output_file}") 64 | 65 | except Exception as e: 66 | print(f"错误: {str(e)}") 67 | return False 68 | 69 | return True 70 | 71 | def get_source_dirs(): 72 | """获取用户输入的源目录列表""" 73 | while True: 74 | dirs_input = input("请输入源目录(用逗号分隔,如: dir1,dir2):").strip() 75 | if not dirs_input: 76 | print("没有提供目录。请至少输入一个目录。") 77 | continue 78 | 79 | # 处理用户输入的目录 80 | source_dirs = [d.strip() for d in dirs_input.split(',')] 81 | # 检查每个目录是否存在 82 | invalid_dirs = [d for d in source_dirs if not os.path.exists(d)] 83 | if invalid_dirs: 84 | print(f"以下目录不存在,请输入有效目录:{', '.join(invalid_dirs)}") 85 | continue 86 | return source_dirs 87 | 88 | def get_file_types(): 89 | """获取用户输入的文件类型列表""" 90 | while True: 91 | types_input = input("请输入要合并的文件类型(用逗号分隔,如: .c,.h):").strip() 92 | if not types_input: 93 | print("请至少输入一个文件类型!") 94 | continue 95 | 96 | # 处理用户输入的文件类型 97 | file_types = [t.strip() for t in types_input.split(',')] 98 | # 确保每个类型都以.开头 99 | file_types = [t if t.startswith('.') else f'.{t}' for t in file_types] 100 | return file_types 101 | 102 | def main(): 103 | # 获取脚本所在目录 104 | script_dir = os.path.dirname(os.path.abspath(__file__)) 105 | 106 | # 获取用户输入的源目录 107 | source_dirs = ['Sources'] # get_source_dirs() 108 | # 将相对路径转换为绝对路径,确保正确解析 109 | source_dirs = [os.path.join(script_dir, d) for d in source_dirs] 110 | 111 | # 获取桌面路径 112 | desktop = os.path.join(os.path.expanduser("~"), "Desktop") 113 | 114 | # 设置输出文件路径 115 | output_file = os.path.join(desktop, "merged_visual_debugger.txt") 116 | 117 | # 获取要合并的文件类型 118 | file_types = ['.swift'] #get_file_types() # 默认可改为 ['.swift'],这里改为用户输入 119 | print(f"\n将合并以下类型的文件: {', '.join(file_types)}") 120 | print(f"源目录: {', '.join(source_dirs)}") 121 | 122 | # 执行合并 123 | success = merge_files(source_dirs, output_file, file_types) 124 | 125 | # 显示结果并等待用户确认 126 | if success: 127 | print("\n合并完成!") 128 | else: 129 | print("\n合并失败!") 130 | 131 | print("\n按Enter键退出...") 132 | input() 133 | 134 | if __name__ == "__main__": 135 | main() 136 | -------------------------------------------------------------------------------- /VisualDebugger.xcworkspace/Playground.playground/Pages/TestImplementation.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import UIKit 5 | import VisualDebugger 6 | 7 | let points3:[CGPoint] = [CGPoint(x:0.563261, y: 0.250566), 8 | CGPoint(x:0.615790, y: 0.251568), 9 | CGPoint(x:0.661398, y: 0.259710), 10 | CGPoint(x:0.699854, y: 0.273847), 11 | CGPoint(x:0.741539, y: 0.304975), 12 | CGPoint(x:0.778933, y: 0.349805), 13 | CGPoint(x:0.804234, y: 0.418417), 14 | CGPoint(x:0.828084, y: 0.488449), 15 | CGPoint(x:0.828877, y: 0.568929), 16 | CGPoint(x:0.824471, y: 0.640248), 17 | CGPoint(x:0.798722, y: 0.685895), 18 | CGPoint(x:0.769048, y: 0.724694), 19 | CGPoint(x:0.731141, y: 0.742849), 20 | CGPoint(x:0.689275, y: 0.753316), 21 | CGPoint(x:0.652157, y: 0.756108), 22 | CGPoint(x:0.608883, y: 0.754526), 23 | CGPoint(x:0.559672, y: 0.741766), 24 | CGPoint(x:0.473791, y: 0.323460), 25 | CGPoint(x:0.448061, y: 0.366450), 26 | CGPoint(x:0.442818, y: 0.418421), 27 | CGPoint(x:0.451221, y: 0.467508), 28 | CGPoint(x:0.473121, y: 0.506806), 29 | CGPoint(x:0.473209, y: 0.568182), 30 | CGPoint(x:0.456134, y: 0.604281), 31 | CGPoint(x:0.450895, y: 0.643387), 32 | CGPoint(x:0.459804, y: 0.681021), 33 | CGPoint(x:0.485578, y: 0.709309), 34 | CGPoint(x:0.503393, y: 0.546038), 35 | CGPoint(x:0.525364, y: 0.552335), 36 | CGPoint(x:0.548114, y: 0.559750), 37 | CGPoint(x:0.571130, y: 0.568064), 38 | CGPoint(x:0.615884, y: 0.505096), 39 | CGPoint(x:0.619026, y: 0.532759), 40 | CGPoint(x:0.621966, y: 0.562555), 41 | CGPoint(x:0.618286, y: 0.587124), 42 | CGPoint(x:0.613874, y: 0.610654), 43 | CGPoint(x:0.513423, y: 0.378370), 44 | CGPoint(x:0.497477, y: 0.411246), 45 | CGPoint(x:0.497527, y: 0.442216), 46 | CGPoint(x:0.519389, y: 0.474055), 47 | CGPoint(x:0.522410, y: 0.441140), 48 | CGPoint(x:0.521293, y: 0.410716), 49 | CGPoint(x:0.523257, y: 0.597611), 50 | CGPoint(x:0.506080, y: 0.627757), 51 | CGPoint(x:0.504997, y: 0.655128), 52 | CGPoint(x:0.521239, y: 0.678628), 53 | CGPoint(x:0.527278, y: 0.653557), 54 | CGPoint(x:0.526351, y: 0.626692), 55 | CGPoint(x:0.696185, y: 0.472936), 56 | CGPoint(x:0.671334, y: 0.507073), 57 | CGPoint(x:0.657819, y: 0.542961), 58 | CGPoint(x:0.661673, y: 0.571214), 59 | CGPoint(x:0.656446, y: 0.595755), 60 | CGPoint(x:0.668570, y: 0.627118), 61 | CGPoint(x:0.693204, y: 0.655391), 62 | CGPoint(x:0.702001, y: 0.628334), 63 | CGPoint(x:0.705933, y: 0.599566), 64 | CGPoint(x:0.706775, y: 0.573984), 65 | CGPoint(x:0.705585, y: 0.543364), 66 | CGPoint(x:0.701212, y: 0.510988), 67 | CGPoint(x:0.680194, y: 0.545555), 68 | CGPoint(x:0.679640, y: 0.572124), 69 | CGPoint(x:0.678558, y: 0.594839), 70 | CGPoint(x:0.684269, y: 0.599869), 71 | CGPoint(x:0.685486, y: 0.577023), 72 | CGPoint(x:0.684801, y: 0.549412), 73 | CGPoint(x:0.596805, y: 0.475715), 74 | CGPoint(x:0.570059, y: 0.488631), 75 | CGPoint(x:0.569375, y: 0.614204), 76 | CGPoint(x:0.596179, y: 0.627464), 77 | CGPoint(x:0.516621, y: 0.272736), 78 | CGPoint(x:0.439406, y: 0.308170), 79 | CGPoint(x:0.370724, y: 0.409735), 80 | CGPoint(x:0.354499, y: 0.520174), 81 | CGPoint(x:0.373587, y: 0.626583), 82 | CGPoint(x:0.441589, y: 0.718513), 83 | CGPoint(x:0.515020, y: 0.747972), 84 | CGPoint(x:0.409239, y: 0.538404)] 85 | 86 | points3.debugView 87 | 88 | -------------------------------------------------------------------------------- /VisualDebugger.xcworkspace/Playground.playground/Pages/TestTinyArea.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import UIKit 4 | import VisualDebugger 5 | 6 | let points3:[CGPoint] = [CGPoint(x:0.563261, y: 0.250566), 7 | CGPoint(x:0.615790, y: 0.251568), 8 | CGPoint(x:0.661398, y: 0.259710), 9 | CGPoint(x:0.699854, y: 0.273847), 10 | CGPoint(x:0.741539, y: 0.304975), 11 | CGPoint(x:0.778933, y: 0.349805), 12 | CGPoint(x:0.804234, y: 0.418417), 13 | CGPoint(x:0.828084, y: 0.488449), 14 | CGPoint(x:0.828877, y: 0.568929), 15 | CGPoint(x:0.824471, y: 0.640248), 16 | CGPoint(x:0.798722, y: 0.685895), 17 | CGPoint(x:0.769048, y: 0.724694), 18 | CGPoint(x:0.731141, y: 0.742849), 19 | CGPoint(x:0.689275, y: 0.753316), 20 | CGPoint(x:0.652157, y: 0.756108), 21 | CGPoint(x:0.608883, y: 0.754526), 22 | CGPoint(x:0.559672, y: 0.741766), 23 | CGPoint(x:0.473791, y: 0.323460), 24 | CGPoint(x:0.448061, y: 0.366450), 25 | CGPoint(x:0.442818, y: 0.418421), 26 | CGPoint(x:0.451221, y: 0.467508), 27 | CGPoint(x:0.473121, y: 0.506806), 28 | CGPoint(x:0.473209, y: 0.568182), 29 | CGPoint(x:0.456134, y: 0.604281), 30 | CGPoint(x:0.450895, y: 0.643387), 31 | CGPoint(x:0.459804, y: 0.681021), 32 | CGPoint(x:0.485578, y: 0.709309), 33 | CGPoint(x:0.503393, y: 0.546038), 34 | CGPoint(x:0.525364, y: 0.552335), 35 | CGPoint(x:0.548114, y: 0.559750), 36 | CGPoint(x:0.571130, y: 0.568064), 37 | CGPoint(x:0.615884, y: 0.505096), 38 | CGPoint(x:0.619026, y: 0.532759), 39 | CGPoint(x:0.621966, y: 0.562555), 40 | CGPoint(x:0.618286, y: 0.587124), 41 | CGPoint(x:0.613874, y: 0.610654), 42 | CGPoint(x:0.513423, y: 0.378370), 43 | CGPoint(x:0.497477, y: 0.411246), 44 | CGPoint(x:0.497527, y: 0.442216), 45 | CGPoint(x:0.519389, y: 0.474055), 46 | CGPoint(x:0.522410, y: 0.441140), 47 | CGPoint(x:0.521293, y: 0.410716), 48 | CGPoint(x:0.523257, y: 0.597611), 49 | CGPoint(x:0.506080, y: 0.627757), 50 | CGPoint(x:0.504997, y: 0.655128), 51 | CGPoint(x:0.521239, y: 0.678628), 52 | CGPoint(x:0.527278, y: 0.653557), 53 | CGPoint(x:0.526351, y: 0.626692), 54 | CGPoint(x:0.696185, y: 0.472936), 55 | CGPoint(x:0.671334, y: 0.507073), 56 | CGPoint(x:0.657819, y: 0.542961), 57 | CGPoint(x:0.661673, y: 0.571214), 58 | CGPoint(x:0.656446, y: 0.595755), 59 | CGPoint(x:0.668570, y: 0.627118), 60 | CGPoint(x:0.693204, y: 0.655391), 61 | CGPoint(x:0.702001, y: 0.628334), 62 | CGPoint(x:0.705933, y: 0.599566), 63 | CGPoint(x:0.706775, y: 0.573984), 64 | CGPoint(x:0.705585, y: 0.543364), 65 | CGPoint(x:0.701212, y: 0.510988), 66 | CGPoint(x:0.680194, y: 0.545555), 67 | CGPoint(x:0.679640, y: 0.572124), 68 | CGPoint(x:0.678558, y: 0.594839), 69 | CGPoint(x:0.684269, y: 0.599869), 70 | CGPoint(x:0.685486, y: 0.577023), 71 | CGPoint(x:0.684801, y: 0.549412), 72 | CGPoint(x:0.596805, y: 0.475715), 73 | CGPoint(x:0.570059, y: 0.488631), 74 | CGPoint(x:0.569375, y: 0.614204), 75 | CGPoint(x:0.596179, y: 0.627464), 76 | CGPoint(x:0.516621, y: 0.272736), 77 | CGPoint(x:0.439406, y: 0.308170), 78 | CGPoint(x:0.370724, y: 0.409735), 79 | CGPoint(x:0.354499, y: 0.520174), 80 | CGPoint(x:0.373587, y: 0.626583), 81 | CGPoint(x:0.441589, y: 0.718513), 82 | CGPoint(x:0.515020, y: 0.747972), 83 | CGPoint(x:0.409239, y: 0.538404)] 84 | 85 | let pnts = Points.init(points: points3, representation: .indices) 86 | pnts.debugView 87 | 88 | let path = UIBezierPath() 89 | path.move(to: .zero) 90 | path.addLine(to: CGPoint(x: 10, y: 0)) 91 | path.close() 92 | 93 | path.debugView 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/Line.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Line.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/5. 6 | // 7 | 8 | // 9 | // Line.swift 10 | // VisualDebugger 11 | // 12 | // Created by chenyungui on 2025/3/5. 13 | // 14 | 15 | import CoreGraphics 16 | #if canImport(UIKit) 17 | import UIKit 18 | #elseif canImport(AppKit) 19 | import AppKit 20 | #endif 21 | import VisualUtils 22 | 23 | public typealias VLine = Line 24 | 25 | public final class Line: SegmentDebugger { 26 | 27 | public let start: CGPoint 28 | public let end: CGPoint 29 | 30 | var center: CGPoint { (start + end) / 2.0 } 31 | 32 | public lazy var vertices: [Vertex] = getVertices(from: [start, end]) 33 | public lazy var edge: MeshEdge = { 34 | createEdge(start: start, end: end, edgeIndex: 0, startIndex: 0, endIndex: 1) 35 | }() 36 | 37 | public init( 38 | start: CGPoint, 39 | end: CGPoint, 40 | name: String? = nil, 41 | transform: Matrix2D = .identity, 42 | color: AppColor = .yellow, 43 | vertexShape: VertexShape = .shape(Circle(radius: 2)), 44 | edgeShape: EdgeShape = .arrow(Arrow()), 45 | edgeStyle: EdgeStyle? = nil, 46 | labelStyle: TextRenderStyle = .nameLabel, 47 | useColorfulLabel: Bool = false 48 | ) { 49 | self.start = start 50 | self.end = end 51 | super.init( 52 | name: name, 53 | transform: transform, 54 | color: color, 55 | vertexShape: vertexShape, 56 | edgeShape: edgeShape, 57 | labelStyle: labelStyle, 58 | useColorfulLable: useColorfulLabel 59 | ) 60 | } 61 | } 62 | 63 | extension Line: DebugRenderable { 64 | public var debugBounds: CGRect? { 65 | let minX = min(start.x, end.x) 66 | let minY = min(start.y, end.y) 67 | let maxX = max(start.x, end.x) 68 | let maxY = max(start.y, end.y) 69 | let rect = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) 70 | return rect * transform 71 | } 72 | 73 | public func render(with transform: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 74 | let targetTransform = self.transform * transform 75 | if displayOptions.contains(.edge) { 76 | edge.render(with: targetTransform, in: context, scale: scale, contextHeight: contextHeight) 77 | } 78 | if displayOptions.contains(.vertex) { 79 | for vertex in self.vertices { 80 | vertex.render(with: targetTransform, in: context, scale: scale, contextHeight: contextHeight) 81 | } 82 | } 83 | } 84 | } 85 | 86 | extension Line { 87 | // 设置边样式 88 | @discardableResult 89 | public func setEdgeStyle( 90 | shape: EdgeShape? = nil, 91 | style: PathStyle? = nil, 92 | label: LabelStyle? = nil, 93 | offset: Double? = nil 94 | ) -> Line { 95 | self.edgeStyleDict[0] = EdgeStyle( 96 | shape: shape, 97 | style: style, 98 | label: label, 99 | offset: offset 100 | ) 101 | return self 102 | } 103 | 104 | // 设置起点样式 105 | @discardableResult 106 | public func setStartStyle( 107 | shape: VertexShape? = nil, 108 | style: PathStyle? = nil, 109 | label: LabelStyle? = nil 110 | ) -> Line { 111 | let style = VertexStyle(shape: shape, style: style, label: label) 112 | self.vertexStyleDict[0] = style 113 | return self 114 | } 115 | 116 | // 设置终点样式 117 | @discardableResult 118 | public func setEndStyle( 119 | shape: VertexShape? = nil, 120 | style: PathStyle? = nil, 121 | label: LabelStyle? = nil 122 | ) -> Line { 123 | let style = VertexStyle(shape: shape, style: style, label: label) 124 | self.vertexStyleDict[1] = style 125 | return self 126 | } 127 | 128 | public func setStartStyle(_ style: VertexStyle) -> Line { 129 | self.vertexStyleDict[0] = style 130 | return self 131 | } 132 | 133 | public func setEndStyle(_ style: VertexStyle) -> Line { 134 | self.vertexStyleDict[1] = style 135 | return self 136 | } 137 | 138 | public func useColorfulLabel(_ value: Bool) -> Line { 139 | self.useColorfulLabel = value 140 | return self 141 | } 142 | 143 | // 显示选项 144 | public func show(_ option: DisplayOptions) -> Self { 145 | self.displayOptions = option 146 | return self 147 | } 148 | 149 | public func log(_ message: Any..., level: Logger.Log.Level = .info) -> Self { 150 | self.logging(message, level: level) 151 | return self 152 | } 153 | } 154 | 155 | #Preview(traits: .fixedLayout(width: 400, height: 420)) { 156 | DebugView { 157 | Line( 158 | start: .init(x: 50, y: 50), 159 | end: .init(x: 250, y: 150), 160 | edgeShape: .line 161 | ) 162 | .setStartStyle(shape: .shape(.empty()), label: "Start") 163 | .setEndStyle(shape: .index, style: .init(color: .red), label: "End") 164 | 165 | Line( 166 | start: .init(x: 50, y: 200), 167 | end: .init(x: 250, y: 180), 168 | color: .green, 169 | edgeShape: .arrow(Arrow(direction: .double)) 170 | ) 171 | .setEdgeStyle(style: .init(color: .red, mode: .fill), label: .string("edge", at: .center)) 172 | .applying(.init(translationX: 0, y: 10)) 173 | } 174 | .coordinateVisible(true) 175 | .coordinateStyle(.default) 176 | .coordinateSystem(.yDown) 177 | } 178 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/Polygon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Points.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/3. 6 | // 7 | 8 | import CoreGraphics 9 | #if canImport(UIKit) 10 | import UIKit 11 | #elseif canImport(AppKit) 12 | import AppKit 13 | #endif 14 | import VisualUtils 15 | 16 | public typealias VPolygon = Polygon 17 | 18 | public final class Polygon: GeometryDebugger { 19 | 20 | public let points: [CGPoint] 21 | public let isClosed: Bool 22 | 23 | public lazy var vertices: [Vertex] = getVertices(from: points) 24 | 25 | public lazy var edges: [MeshEdge] = { 26 | points.segments(isClosed: isClosed).enumerated().map { (i, seg) in 27 | createEdge(start: seg.start, end: seg.end, edgeIndex: i, startIndex: i, endIndex: i+1) 28 | } 29 | }() 30 | 31 | public lazy var face: MeshFace = { 32 | createFace(vertices: points, faceIndex: 0) 33 | }() 34 | 35 | public init( 36 | _ points: [CGPoint], 37 | name: String? = nil, 38 | transform: Matrix2D = .identity, 39 | isClosed: Bool = true, 40 | vertexShape: VertexShape = .shape(Circle(radius: 2)), 41 | edgeShape: EdgeShape = .arrow(Arrow()), 42 | color: AppColor = .yellow, 43 | vertexStyleDict: [Int: VertexStyle] = [:], 44 | edgeStyleDict: [Int: EdgeStyle] = [:], 45 | displayOptions: DisplayOptions = .all, 46 | labelStyle: TextRenderStyle = .nameLabel, 47 | useColorfulLabel: Bool = false 48 | ) { 49 | self.points = points 50 | self.isClosed = isClosed 51 | 52 | super.init( 53 | name: name, 54 | transform: transform, 55 | vertexShape: vertexShape, 56 | edgeShape: edgeShape, 57 | color: color, 58 | vertexStyleDict: vertexStyleDict, 59 | edgeStyleDict: edgeStyleDict, 60 | displayOptions: displayOptions, 61 | labelStyle: labelStyle, 62 | useColorfulLable: useColorfulLabel 63 | ) 64 | } 65 | 66 | public func setVertexStyle( 67 | at index: Int, 68 | shape: VertexShape? = nil, 69 | style: PathStyle? = nil, 70 | label: LabelStyle? = nil 71 | ) -> Polygon { 72 | guard index < points.count else { return self } 73 | let style = VertexStyle(shape: shape, style: style, label: label) 74 | self.vertexStyleDict[index] = style 75 | return self 76 | } 77 | 78 | public func setVertexStyle( 79 | _ style: VertexStyle, 80 | for indices: Set 81 | ) -> Polygon { 82 | for index in indices where index < points.count { 83 | self.vertexStyleDict[index] = style 84 | } 85 | return self 86 | } 87 | 88 | public func setEdgeStyle( 89 | at index: Int, 90 | shape: EdgeShape? = nil, 91 | style: PathStyle? = nil, 92 | label: LabelStyle? = nil, 93 | offset: Double? = nil 94 | ) -> Polygon { 95 | guard index < points.count - 1 || (index == points.count - 1 && isClosed) else { return self } 96 | let edgeStyle = EdgeStyle( 97 | shape: shape, 98 | style: style, 99 | label: label, 100 | offset: offset 101 | ) 102 | edgeStyleDict[index] = edgeStyle 103 | return self 104 | } 105 | 106 | public func setFaceStyle( 107 | style: PathStyle? = nil, 108 | label: LabelStyle? = nil 109 | ) -> Polygon { 110 | let style = FaceStyle( 111 | style: style, 112 | label: label 113 | ) 114 | self.faceStyleDict[0] = style 115 | return self 116 | } 117 | 118 | public func useColorfulLabel(_ value: Bool) -> Self { 119 | self.useColorfulLabel = value 120 | return self 121 | } 122 | 123 | // MARK: - modifier 124 | public func show(_ option: DisplayOptions) -> Self { 125 | self.displayOptions = option 126 | return self 127 | } 128 | 129 | public func log(_ message: Any..., level: Logger.Log.Level = .info) -> Self { 130 | self.logging(message, level: level) 131 | return self 132 | } 133 | } 134 | 135 | extension Polygon: DebugRenderable { 136 | public var debugBounds: CGRect? { 137 | guard let bounds = points.bounds else { return nil } 138 | return bounds * transform 139 | } 140 | public func render(with transform: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 141 | let matrix = self.transform * transform 142 | if displayOptions.contains(.face) { 143 | face.render(with: matrix, in: context, scale: scale, contextHeight: contextHeight) 144 | } 145 | if displayOptions.contains(.edge) { 146 | for edge in edges { 147 | edge.render(with: matrix, in: context, scale: scale, contextHeight: contextHeight) 148 | } 149 | } 150 | if displayOptions.contains(.vertex) { 151 | for vtx in vertices { 152 | vtx.render(with: matrix, in: context, scale: scale, contextHeight: contextHeight) 153 | } 154 | } 155 | } 156 | } 157 | 158 | #Preview(traits: .fixedLayout(width: 400, height: 420)) { 159 | DebugView { 160 | Polygon([ 161 | .init(x: 40, y: 10), 162 | .init(x: 10, y: 23), 163 | .init(x: 23, y: 67) 164 | ], vertexShape: .index) 165 | .setVertexStyle(at: 0, shape: .shape(Circle(radius: 2)), label: "Corner") 166 | .setVertexStyle(at: 1, style: .init(color: .red), label: .coordinate()) 167 | .setEdgeStyle(at: 2, shape: .arrow(.doubleArrow), style: .init(color: .red, mode: .fill), label: .string("edge", rotatable: true)) 168 | .setFaceStyle(label: .string("face", at: .center)) 169 | .show([.vertex, .edge, .face]) 170 | } 171 | .coordinateVisible(true) 172 | .coordinateStyle(.default) 173 | .coordinateSystem(.yDown) 174 | //.zoom(1.5, aroundCenter: .init(x: 10, y: 23)) 175 | } 176 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/base/SegmentDebugger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SegmentDebugger.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/5. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | public class SegmentDebugger: VertexDebugger { 12 | public let edgeShape: EdgeShape 13 | public var edgeStyleDict: [Int: EdgeStyle] = [:] 14 | 15 | public init( 16 | name: String? = nil, 17 | transform: Matrix2D, 18 | color: AppColor, 19 | vertexShape: VertexDebugger.VertexShape = .shape(Circle(radius: 2)), 20 | edgeShape: EdgeShape = .arrow(Arrow()), 21 | displayOptions: DisplayOptions = .all, 22 | labelStyle: TextRenderStyle = .nameLabel, 23 | useColorfulLable: Bool = false, 24 | vertexStyleDict: [Int: VertexStyle] = [:], 25 | edgeStyleDict: [Int: EdgeStyle] = [:] 26 | ) { 27 | self.edgeShape = edgeShape 28 | self.edgeStyleDict = edgeStyleDict 29 | super.init( 30 | name: name, 31 | transform: transform, 32 | color: color, 33 | vertexShape: vertexShape, 34 | vertexStyleDict: vertexStyleDict, 35 | displayOptions: displayOptions, 36 | labelStyle: labelStyle, 37 | useColorfulLable: useColorfulLable 38 | ) 39 | } 40 | 41 | func getRadius(index: Int) -> Double { 42 | let shape = self.vertexStyleDict[index]?.shape ?? vertexShape 43 | switch shape { 44 | case .shape(let shape): return shape.radius 45 | case .index: return 6 46 | } 47 | } 48 | 49 | func getEdgeRenderStyle(style: PathStyle?) -> ShapeRenderStyle { 50 | let color = style?.color ?? color 51 | guard let mode = style?.mode else { 52 | return ShapeRenderStyle( 53 | stroke: .init(color: color, style: .init(lineWidth: 1)), 54 | fill: nil 55 | ) 56 | } 57 | switch mode { 58 | case .stroke(dashed: let dashed): 59 | let dash: [CGFloat] = dashed ? [5, 5] : [] 60 | return ShapeRenderStyle( 61 | stroke: .init(color: color, style: .init(lineWidth: 1, dash: dash)), 62 | fill: .init(color: color, style: .init()) 63 | ) 64 | case .fill: 65 | return ShapeRenderStyle( 66 | stroke: .init(color: color, style: .init(lineWidth: 1)), 67 | fill: .init(color: color, style: .init()) 68 | ) 69 | } 70 | } 71 | 72 | func createEdge( 73 | start: CGPoint, 74 | end: CGPoint, 75 | edgeIndex: Int, 76 | startIndex: Int, 77 | endIndex: Int 78 | ) -> SegmentRenderElement { 79 | let customStyle = edgeStyleDict[edgeIndex] 80 | let edgeShape = customStyle?.shape ?? self.edgeShape 81 | let source: SegmentRenderer? = switch edgeShape { 82 | case .line: nil 83 | case .arrow(let arrow): arrow 84 | } 85 | 86 | var labelString: String? 87 | if let edgeLabel = customStyle?.label?.text { 88 | switch edgeLabel { 89 | case .string(let string): 90 | labelString = string 91 | case .coordinate: 92 | labelString = "\((start + end) / 2)" 93 | case .index: 94 | labelString = "\(edgeIndex)" 95 | default: 96 | break 97 | } 98 | } 99 | var label: TextElement? 100 | if let labelString, let labelStyle = customStyle?.label?.style { 101 | label = TextElement(text: labelString, style: labelStyle) 102 | } else { 103 | label = TextElement( 104 | text: labelString, 105 | defaultStyle: labelStyle, 106 | location: customStyle?.label?.location ?? .center, 107 | textColor: useColorfulLabel ? customStyle?.style?.color ?? self.color : nil, 108 | rotatable: customStyle?.label?.rotatable ?? false 109 | ) 110 | } 111 | return SegmentRenderElement( 112 | start: start, 113 | end: end, 114 | transform: transform, 115 | segmentShape: source, 116 | segmentStyle: getEdgeRenderStyle(style: customStyle?.style), 117 | centerElement: label, 118 | offset: customStyle?.offset ?? 0, 119 | startOffset: getRadius(index: startIndex), 120 | endOffset: getRadius(index: endIndex) 121 | ) 122 | } 123 | 124 | func getVertices(from points: [CGPoint]) -> [Vertex] { 125 | points.enumerated().map { (i, point) in 126 | createVertex(index: i, position: point) 127 | } 128 | } 129 | func getMeshEdges(vertices: [CGPoint], edges: [Edge]) -> [MeshEdge] { 130 | return edges.enumerated().map { (i, edge) in 131 | createEdge( 132 | start: vertices[edge.org], 133 | end: vertices[edge.dst], 134 | edgeIndex: i, 135 | startIndex: edge.org, 136 | endIndex: edge.dst 137 | ) 138 | } 139 | } 140 | } 141 | 142 | extension SegmentDebugger { 143 | public typealias MeshEdge = SegmentRenderElement 144 | 145 | public enum EdgeShape { 146 | case line 147 | case arrow(Arrow) 148 | } 149 | public struct EdgeStyle { 150 | let shape: EdgeShape? 151 | let style: PathStyle? 152 | let label: LabelStyle? 153 | let offset: Double? 154 | 155 | public init(shape: EdgeShape?, style: PathStyle?, label: LabelStyle?, offset: Double?) { 156 | self.shape = shape 157 | self.style = style 158 | self.label = label 159 | self.offset = offset 160 | } 161 | } 162 | } 163 | 164 | extension SegmentDebugger { 165 | public struct Edge { 166 | public var org: Int 167 | public var dst: Int 168 | 169 | public init(org: Int, dst: Int) { 170 | self.org = org 171 | self.dst = dst 172 | } 173 | } 174 | } 175 | 176 | extension SegmentDebugger.Edge: Equatable { 177 | public static func ==(lhs: Self, rhs: Self) -> Bool { 178 | // 只要是使用的相同的两个顶点,就代表相等,不考虑顶点顺序 179 | return (lhs.org == rhs.org && lhs.dst == rhs.dst) || (lhs.org == rhs.dst && lhs.dst == rhs.org) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Sources/VisualUtils/CGRect+Behavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRect.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | 9 | import CoreGraphics 10 | 11 | public extension CGRect { 12 | 13 | // MARK: - top 14 | init(topLeft point: CGPoint, size: CGSize) { 15 | self.init(origin: point, size: size) 16 | } 17 | 18 | init(topCenter point: CGPoint, size: CGSize) { 19 | let origin = CGPoint(x: point.x - size.width / 2, y: point.y) 20 | self.init(origin: origin, size: size) 21 | } 22 | 23 | init(topRight point: CGPoint, size: CGSize) { 24 | let origin = CGPoint(x: point.x - size.width, y: point.y) 25 | self.init(origin: origin, size: size) 26 | } 27 | 28 | // MARK: - middle 29 | init(middleLeft point: CGPoint, size: CGSize) { 30 | let origin = CGPoint(x: point.x, y: point.y - size.height / 2) 31 | self.init(origin: origin, size: size) 32 | } 33 | 34 | init(center point: CGPoint, size: CGSize) { 35 | let origin = CGPoint(x: point.x - size.width / 2, y: point.y - size.height / 2) 36 | self.init(origin: origin, size: size) 37 | } 38 | 39 | init(middleRight point: CGPoint, size: CGSize) { 40 | let origin = CGPoint(x: point.x - size.width, y: point.y - size.height / 2) 41 | self.init(origin: origin, size: size) 42 | } 43 | 44 | // MARK: - right 45 | init(bottomLeft point: CGPoint, size: CGSize) { 46 | let origin = CGPoint(x: point.x, y: point.y - size.height) 47 | self.init(origin: origin, size: size) 48 | } 49 | 50 | init(bottomCenter point: CGPoint, size: CGSize) { 51 | let origin = CGPoint(x: point.x - size.width / 2, y: point.y - size.height) 52 | self.init(origin: origin, size: size) 53 | } 54 | 55 | init(bottomRight point: CGPoint, size: CGSize) { 56 | let origin = CGPoint(x: point.x - size.width, y: point.y - size.height) 57 | self.init(origin: origin, size: size) 58 | } 59 | 60 | init(anchor: Anchor, center: CGPoint, size: CGSize) { 61 | switch anchor { 62 | case .topLeft: self = CGRect(topLeft: center, size: size) 63 | case .topCenter: self = CGRect(topCenter: center, size: size) 64 | case .topRight: self = CGRect(topRight: center, size: size) 65 | case .midLeft: self = CGRect(middleLeft: center, size: size) 66 | case .midCenter: self = CGRect(center: center, size: size) 67 | case .midRight: self = CGRect(middleRight: center, size: size) 68 | case .btmLeft: self = CGRect(bottomLeft: center, size: size) 69 | case .btmCenter: self = CGRect(bottomCenter: center, size: size) 70 | case .btmRight: self = CGRect(bottomRight: center, size: size) 71 | } 72 | } 73 | 74 | func expanding(by insets: AppEdgeInsets) -> CGRect { 75 | 76 | let x = self.origin.x - insets.left 77 | let y = self.origin.y - insets.top 78 | let w = self.size.width + (insets.left + insets.right) 79 | let h = self.size.height + (insets.top + insets.bottom) 80 | return CGRect(x: x, y: y, width: w, height: h) 81 | } 82 | 83 | func shrinked(_ value: CGFloat) -> CGRect { 84 | return self.insetBy(dx: value, dy: value) 85 | } 86 | 87 | func fit(to rect: CGRect, config: FitConfig) -> CGAffineTransform { 88 | RectFitter.fit(rect: self, to: rect, config: config) 89 | } 90 | 91 | func fitted(to rect: CGRect, config: FitConfig) -> CGRect { 92 | let t = self.fit(to: rect, config: config) 93 | return self.applying(t) 94 | } 95 | } 96 | 97 | public extension CGRect { 98 | func offseted(_ offset: CGPoint) -> CGRect { 99 | return CGRect(origin: self.origin + offset, size: self.size) 100 | } 101 | 102 | func offseted(x: CGFloat, y: CGFloat) -> CGRect { 103 | return CGRect( 104 | x: self.origin.x + x, 105 | y: self.origin.y + y, 106 | width: self.size.width, 107 | height: self.size.height 108 | ) 109 | } 110 | } 111 | 112 | public extension CGRect { 113 | var topLeft: CGPoint { 114 | return CGPoint(x: self.minX, y: self.minY) 115 | } 116 | 117 | var topCenter: CGPoint { 118 | return CGPoint(x: self.midX, y: self.minY) 119 | } 120 | 121 | var topRight: CGPoint { 122 | return CGPoint(x: self.maxX, y: self.minY) 123 | } 124 | 125 | var middleLeft: CGPoint { 126 | return CGPoint(x: self.minX, y: self.midY) 127 | } 128 | 129 | var middleRight: CGPoint { 130 | return CGPoint(x: self.maxX, y: self.midY) 131 | } 132 | 133 | var bottomLeft: CGPoint { 134 | return CGPoint(x: self.minX, y: self.maxY) 135 | } 136 | 137 | var bottomCenter: CGPoint { 138 | return CGPoint(x: self.midX, y: self.maxY) 139 | } 140 | 141 | var bottomRight: CGPoint { 142 | return CGPoint(x: self.maxX, y: self.maxY) 143 | } 144 | 145 | var center: CGPoint { 146 | return CGPoint( 147 | x: self.origin.x + self.size.width / 2, 148 | y: self.origin.y + self.size.height / 2 149 | ) 150 | } 151 | 152 | func getAnchor(_ anchor: Anchor) -> CGPoint { 153 | switch anchor { 154 | case .topLeft: return self.topLeft 155 | case .topCenter: return self.topCenter 156 | case .topRight: return self.topRight 157 | 158 | case .midLeft: return self.middleLeft 159 | case .midCenter: return self.center 160 | case .midRight: return self.middleRight 161 | 162 | case .btmLeft: return self.bottomLeft 163 | case .btmCenter: return self.bottomCenter 164 | case .btmRight: return self.bottomRight 165 | } 166 | } 167 | } 168 | 169 | public extension CGRect { 170 | var rectFromOrigin: CGRect { 171 | let minX = min(0, self.minX) 172 | let minY = min(0, self.minY) 173 | let maxX = max(0, self.maxX) 174 | let maxY = max(0, self.maxY) 175 | return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) 176 | } 177 | } 178 | 179 | public extension Array where Element == CGRect { 180 | 181 | var bounds: CGRect? { 182 | guard !self.isEmpty else { return nil } 183 | var rect = self[0] 184 | for i in 1..= 0 && edge.org < vertexCount, "顶点索引 \(edge.org) 超出范围") 57 | precondition(edge.dst >= 0 && edge.dst < vertexCount, "顶点索引 \(edge.dst) 超出范围") 58 | } 59 | 60 | self.edges = edges 61 | 62 | super.init( 63 | name: name, 64 | transform: transform, 65 | color: color, 66 | vertexShape: vertexShape, 67 | edgeShape: edgeShape, 68 | displayOptions: displayOptions, 69 | labelStyle: labelStyle, 70 | useColorfulLable: useColorfulLabel, 71 | vertexStyleDict: vertexStyleDict, 72 | edgeStyleDict: edgeStyleDict 73 | ) 74 | } 75 | 76 | // 自定义方法:设置顶点样式 77 | public func setVertexStyle( 78 | at index: Int, 79 | shape: VertexShape? = nil, 80 | style: PathStyle? = nil, 81 | label: LabelStyle? = nil 82 | ) -> Lines { 83 | guard index < vertices.count else { return self } 84 | let style = VertexStyle(shape: shape, style: style, label: label) 85 | self.vertexStyleDict[index] = style 86 | return self 87 | } 88 | 89 | public func setVertexStyle( 90 | _ style: VertexStyle, 91 | for indices: Set 92 | ) -> Lines { 93 | for index in indices where index < vertices.count { 94 | self.vertexStyleDict[index] = style 95 | } 96 | return self 97 | } 98 | 99 | // 自定义方法:设置边样式 100 | public func setEdgeStyle( 101 | for edge: Edge, 102 | shape: EdgeShape? = nil, 103 | style: PathStyle? = nil, 104 | label: LabelStyle? = nil 105 | ) -> Lines { 106 | if let edgeIndex = edges.firstIndex(of: edge) { 107 | return self.setEdgeStyle( 108 | at: edgeIndex, 109 | shape: shape, 110 | style: style, 111 | name: label 112 | ) 113 | } else { 114 | return self 115 | } 116 | } 117 | 118 | public func setEdgeStyle( 119 | at index: Int, 120 | shape: EdgeShape? = nil, 121 | style: PathStyle? = nil, 122 | name: LabelStyle? = nil, 123 | offset: Double? = nil 124 | ) -> Lines { 125 | let edgeStyle = EdgeStyle( 126 | shape: shape, 127 | style: style, 128 | label: name, 129 | offset: offset 130 | ) 131 | edgeStyleDict[index] = edgeStyle 132 | return self 133 | } 134 | 135 | // MARK: - modifier 136 | public func show(_ option: DisplayOptions) -> Self { 137 | self.displayOptions = option 138 | return self 139 | } 140 | 141 | public func log(_ message: Any..., level: Logger.Log.Level = .info) -> Self { 142 | self.logging(message, level: level) 143 | return self 144 | } 145 | } 146 | 147 | extension Lines { 148 | public var indices: [Int] { 149 | self.edges.map{ [$0.org, $0.dst] }.flatMap{ $0 } 150 | } 151 | } 152 | 153 | extension Lines: DebugRenderable { 154 | public var debugBounds: CGRect? { 155 | guard let bounds = vertices.bounds else { return nil } 156 | return bounds * transform 157 | } 158 | 159 | public func render(with transform: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 160 | let matrix = self.transform * transform 161 | // 然后渲染边 162 | if displayOptions.contains(.edge) { 163 | for edge in edgeElements { 164 | edge.render(with: matrix, in: context, scale: scale, contextHeight: contextHeight) 165 | } 166 | } 167 | 168 | // 最后渲染顶点 169 | if displayOptions.contains(.vertex) { 170 | for vertex in vertexElements { 171 | vertex.render(with: matrix, in: context, scale: scale, contextHeight: contextHeight) 172 | } 173 | } 174 | } 175 | } 176 | 177 | 178 | #Preview(traits: .fixedLayout(width: 400, height: 420)) { 179 | let vertices = [ 180 | CGPoint(x: 50, y: 50), 181 | CGPoint(x: 150, y: 50), 182 | CGPoint(x: 100, y: 150), 183 | CGPoint(x: 200, y: 150) 184 | ] 185 | 186 | let indices = [0, 1, 1, 2, 2, 3] // 三条连续的线段 187 | 188 | DebugView(showOrigin: true) { 189 | Lines(vertices, indices: indices) 190 | .setVertexStyle(at: 0, shape: .index, label: .coordinate(at: .top)) 191 | .setVertexStyle(at: 1, style: .init(color: .red), label: "顶点1") 192 | .setEdgeStyle(for: .init(org: 2, dst: 3), style: .init(color: .green)) 193 | .show([.vertex, .edge]) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Sources/VisualUtils/RectFitter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectFitter.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/9. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias FitConfig = RectFitter.Config 11 | 12 | public struct RectFitter { 13 | 14 | public static func fit( 15 | rect source: CGRect, 16 | to target: CGRect, 17 | config: Config = .aspectFillInside 18 | ) -> CGAffineTransform { 19 | Self.fit( 20 | rect: source, 21 | to: target, 22 | align: config.align, 23 | scale: config.scale 24 | ) 25 | } 26 | 27 | public static func fit( 28 | rect source: CGRect, 29 | to target: CGRect, 30 | align: Align, 31 | scale: Scale 32 | ) -> CGAffineTransform { 33 | 34 | let x1 = source.origin.x 35 | let y1 = source.origin.y 36 | let w1 = source.size.width 37 | let h1 = source.size.height 38 | 39 | let w2 = target.size.width 40 | let h2 = target.size.height 41 | 42 | var scaleX: CGFloat = 1 43 | var scaleY: CGFloat = 1 44 | 45 | switch scale { 46 | case .none: 47 | break 48 | case .aspect(let match): 49 | let useWidth: Bool = switch match { 50 | case .width: true 51 | case .height: false 52 | case .minEdge: w1 / h1 > w2 / h2 53 | case .maxEdge: w1 / h1 < w2 / h2 54 | } 55 | scaleX = useWidth ? w2 / w1 : h2 / h1 56 | scaleY = scaleX 57 | case .stretch(let match): 58 | if match.contains(.width) { 59 | scaleX = w2 / w1 60 | } 61 | if match.contains(.height) { 62 | scaleY = h2 / h1 63 | } 64 | } 65 | // scaled rect 66 | let rect1 = CGRect( 67 | x: scaleX * x1 - x1, 68 | y: scaleY * y1 - y1, 69 | width: w1 * scaleX, 70 | height: h1 * scaleY 71 | ) 72 | // match anchor based on alignment 73 | let anchor1 = rect1.getPoint(align.anchor) 74 | let anchor2 = target.getPoint(align.anchor) 75 | let tx = anchor2.x - anchor1.x - x1 76 | let ty = anchor2.y - anchor1.y - y1 77 | 78 | return CGAffineTransform(a: scaleX, b: 0, c: 0, d: scaleY, tx: tx, ty: ty) 79 | } 80 | } 81 | 82 | extension RectFitter { 83 | public enum HAlign: Int, Sendable { 84 | case left = 1 // 1 << 0 85 | case center = 2 // 1 << 1 86 | case right = 4 // 1 << 2 87 | 88 | var ratio: Double { 89 | switch self { 90 | case .left: 0 91 | case .center: 0.5 92 | case .right: 1 93 | } 94 | } 95 | } 96 | } 97 | 98 | extension RectFitter { 99 | public enum VAlign: Int, Sendable { 100 | case top = 8 // 1 << 3 101 | case center = 16 // 1 << 4 102 | case bottom = 32 // 1 << 5 103 | 104 | var ratio: Double { 105 | switch self { 106 | case .top: 0 107 | case .center: 0.5 108 | case .bottom: 1 109 | } 110 | } 111 | } 112 | } 113 | 114 | extension RectFitter { 115 | public enum Align: Int, Sendable { 116 | case topLeft = 9 //VAlign.top.rawValue | HAlign.left.rawValue 117 | case topCenter = 10 //VAlign.top.rawValue | HAlign.center.rawValue 118 | case topRight = 12 //VAlign.top.rawValue | HAlign.right.rawValue 119 | 120 | case midLeft = 17 //VAlign.center.rawValue | HAlign.left.rawValue 121 | case midCenter = 18 //VAlign.center.rawValue | HAlign.center.rawValue 122 | case midRight = 20 //VAlign.center.rawValue | HAlign.right.rawValue 123 | 124 | case btmLeft = 33 //VAlign.bottom.rawValue | HAlign.left.rawValue 125 | case btmCenter = 34 //VAlign.bottom.rawValue | HAlign.center.rawValue 126 | case btmRight = 36 //VAlign.bottom.rawValue | HAlign.right.rawValue 127 | 128 | var hAlign: HAlign { 129 | switch self { 130 | case .topLeft, .midLeft, .btmLeft: .left 131 | case .topCenter, .midCenter, .btmCenter: .center 132 | case .topRight, .midRight, .btmRight: .right 133 | } 134 | } 135 | 136 | var vAlign: VAlign { 137 | switch self { 138 | case .topLeft, .topCenter, .topRight: .top 139 | case .midLeft, .midCenter, .midRight: .center 140 | case .btmLeft, .btmCenter, .btmRight: .bottom 141 | } 142 | } 143 | 144 | var anchor: CGPoint { 145 | CGPoint(x: hAlign.ratio, y: vAlign.ratio) 146 | } 147 | 148 | init(hAlign: HAlign, vAlign: VAlign) { 149 | self.init(rawValue: hAlign.rawValue | vAlign.rawValue)! 150 | } 151 | } 152 | } 153 | 154 | extension RectFitter { 155 | public enum Scale: Sendable { 156 | public enum Length: Sendable { 157 | case width, height, minEdge, maxEdge 158 | } 159 | public struct Edge: OptionSet, Sendable { 160 | public let rawValue: Int 161 | public init(rawValue: Int) { 162 | self.rawValue = rawValue 163 | } 164 | public static let width = Self(rawValue: 1 << 0) 165 | public static let height = Self(rawValue: 1 << 1) 166 | public static let all: Self = [width, height] 167 | } 168 | case none 169 | case aspect(match: Length) 170 | case stretch(match: Edge) 171 | } 172 | } 173 | 174 | extension RectFitter { 175 | public struct Config: Sendable { 176 | var align: Align 177 | var scale: Scale 178 | 179 | init(align: Align, scale: Scale) { 180 | self.align = align 181 | self.scale = scale 182 | } 183 | } 184 | } 185 | 186 | public extension RectFitter.Config { 187 | static let aspectFillInside: Self = .init(align: .midCenter, scale: .aspect(match: .minEdge)) 188 | static let aspectFillOutside: Self = .init(align: .midCenter, scale: .aspect(match: .maxEdge)) 189 | static let stretchFill: Self = .init(align: .midCenter, scale: .stretch(match: .all)) 190 | static let alignCenter: Self = .init(align: .midCenter, scale: .none) 191 | } 192 | 193 | extension CGRect { 194 | fileprivate func getPoint(_ anchor: CGPoint) -> CGPoint { 195 | return CGPoint( 196 | x: self.origin.x + self.size.width * anchor.x, 197 | y: self.origin.y + self.size.height * anchor.y 198 | ) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/DebugView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugView.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/2/26. 6 | // 7 | 8 | 9 | #if canImport(UIKit) 10 | import UIKit 11 | #elseif canImport(AppKit) 12 | import AppKit 13 | #endif 14 | import VisualUtils 15 | 16 | public final class DebugView: AppView { 17 | 18 | var context: DebugContext 19 | 20 | public var elements: [any ContextRenderable] { 21 | get { context.elements } 22 | set { 23 | context.elements = newValue 24 | self.refresh() 25 | } 26 | } 27 | 28 | public init( 29 | minWidth: Double = 250, 30 | numSegments: Int = 5, 31 | showOrigin: Bool = false, 32 | showCoordinate: Bool = true, 33 | showLog: Bool = true, 34 | showGlobalElements: Bool = true, 35 | coordinateSystem: CoordinateSystem2D = .yDown, 36 | coordinateStyle: CoordinateStyle = .default, 37 | elements: [any DebugRenderable] 38 | ) { 39 | let context = DebugContext( 40 | minWidth: minWidth, 41 | numSegments: numSegments, 42 | showOrigin: showOrigin, 43 | showCoordinate: showCoordinate, 44 | showLog: showLog, 45 | showGlobalElements: showGlobalElements, 46 | coordinateSystem: coordinateSystem, 47 | coordinateStyle: coordinateStyle, 48 | elements: elements 49 | ) 50 | self.context = context 51 | super.init(frame: context.frame) 52 | self.translatesAutoresizingMaskIntoConstraints = false 53 | #if os(macOS) 54 | self.wantsLayer = true 55 | #endif 56 | } 57 | 58 | public init( 59 | minWidth: Double = 250, 60 | numSegments: Int = 5, 61 | showOrigin: Bool = false, 62 | showCoordinate: Bool = true, 63 | showLog: Bool = true, 64 | showGlobalElements: Bool = true, 65 | coordinateSystem: CoordinateSystem2D = .yDown, 66 | coordinateStyle: CoordinateStyle = .default, 67 | @DebugRenderBuilder _ builder: () -> [any DebugRenderable] 68 | ) { 69 | let context = DebugContext( 70 | minWidth: minWidth, 71 | numSegments: numSegments, 72 | showOrigin: showOrigin, 73 | showCoordinate: showCoordinate, 74 | showLog: showLog, 75 | showGlobalElements: showGlobalElements, 76 | coordinateSystem: coordinateSystem, 77 | coordinateStyle: coordinateStyle, 78 | elements: builder() 79 | ) 80 | self.context = context 81 | super.init(frame: context.frame) 82 | self.translatesAutoresizingMaskIntoConstraints = false 83 | #if os(macOS) 84 | self.wantsLayer = true 85 | #endif 86 | } 87 | 88 | public init( 89 | debugRect: CGRect, 90 | minWidth: Double = 250, 91 | numSegments: Int = 5, 92 | showOrigin: Bool = false, 93 | showCoordinate: Bool = true, 94 | coordinateSystem: CoordinateSystem2D = .yDown, 95 | coordinateStyle: CoordinateStyle = .default, 96 | @RenderBuilder _ builder: () -> [any ContextRenderable] 97 | ) { 98 | let elements = builder() 99 | let context = DebugContext( 100 | debugRect: debugRect, 101 | elements: elements, 102 | minWidth: minWidth, 103 | numSegments: numSegments, 104 | showOrigin: showOrigin, 105 | showCoordinate: showCoordinate, 106 | coordinateSystem: coordinateSystem, 107 | coordinateStyle: coordinateStyle 108 | ) 109 | self.context = context 110 | super.init(frame: context.frame) 111 | self.translatesAutoresizingMaskIntoConstraints = false 112 | #if os(macOS) 113 | self.wantsLayer = true 114 | #endif 115 | } 116 | 117 | required init?(coder: NSCoder) { 118 | fatalError("init(coder:) has not been implemented") 119 | } 120 | 121 | public func append(_ element: ContextRenderable) { 122 | self.elements.append(element) 123 | } 124 | 125 | public func showLog(_ show: Bool) -> DebugView { 126 | context.showLog = show 127 | self.refresh() 128 | return self 129 | } 130 | 131 | public func showGlobalElements(_ show: Bool) -> DebugView { 132 | context.showGlobalElements = show 133 | self.refresh() 134 | return self 135 | } 136 | 137 | public func coordinateVisible(_ show: Bool) -> DebugView { 138 | context.showCoordinate = show 139 | self.refresh() 140 | return self 141 | } 142 | 143 | public func coordinateSystem( _ coord: CoordinateSystem2D) -> DebugView { 144 | context.coordinateSystem = coord 145 | self.refresh() 146 | return self 147 | } 148 | 149 | public func coordinateStyle(_ style: CoordinateStyle) -> DebugView { 150 | context.coordinateStyle = style 151 | self.refresh() 152 | return self 153 | } 154 | 155 | public func zoom(_ zoom: Double, aroundCenter center: CGPoint? = nil) -> DebugView { 156 | self.context.zoom(zoom, aroundCenter: center) 157 | self.refresh() 158 | return self 159 | } 160 | 161 | public func log(_ message: Any..., level: Logger.Log.Level = .info) -> DebugView { 162 | context.logging(message, level: level) 163 | return self 164 | } 165 | 166 | private func refresh() { 167 | #if os(iOS) 168 | self.setNeedsDisplay() 169 | #elseif os(macOS) 170 | self.setNeedsDisplay(self.bounds) 171 | #endif 172 | } 173 | 174 | #if os(iOS) 175 | public override func draw(_ rect: CGRect) { 176 | guard let context = UIGraphicsGetCurrentContext() else { return } 177 | context.clear(rect) 178 | let scale: CGFloat = self.contentScaleFactor 179 | let contextHeight = Int(bounds.height * scale) 180 | self.context.render(in: context, scale: scale, contextHeight: contextHeight) 181 | } 182 | #elseif os(macOS) 183 | 184 | public override func draw(_ dirtyRect: NSRect) { 185 | guard let context = NSGraphicsContext.current?.cgContext else { return } 186 | let scale: CGFloat = 1 // self.layer?.contentsScale ?? 1 187 | let contextHeight = Int(bounds.height * scale) 188 | self.context.render(in: context, scale: scale, contextHeight: contextHeight) 189 | } 190 | 191 | public override var isFlipped: Bool { 192 | return true // 使坐标系从左上角开始(可选) 193 | } 194 | #endif 195 | } 196 | 197 | 198 | extension DebugView { 199 | public static func build( 200 | @DebugBuilder builder: () -> [any Debuggable] 201 | ) -> DebugView { 202 | let elements = builder().flatMap { $0.debugElements } 203 | return DebugView(elements: elements) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/VectorMesh.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VectorMesh.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/11. 6 | // 7 | 8 | import CoreGraphics 9 | #if canImport(UIKit) 10 | import UIKit 11 | #elseif canImport(AppKit) 12 | import AppKit 13 | #endif 14 | import VisualUtils 15 | 16 | public final class VectorMesh: GeometryDebugger { 17 | 18 | public let faces: [VectorTriangle] 19 | 20 | public lazy var faceElements: [VectorTriangleRenderElement] = getMeshFaces() 21 | 22 | public init( 23 | faces: [VectorTriangle], 24 | name: String? = nil, 25 | transform: Matrix2D = .identity, 26 | color: AppColor = .yellow, 27 | faceStyleDict: [Int: FaceStyle] = [:], 28 | displayOptions: DisplayOptions = .face, 29 | labelStyle: TextRenderStyle = .nameLabel, 30 | useColorfulLabel: Bool = false 31 | ) { 32 | self.faces = faces 33 | 34 | super.init( 35 | name: name, 36 | transform: transform, 37 | color: color, 38 | faceStyleDict: faceStyleDict, 39 | displayOptions: displayOptions, 40 | labelStyle: labelStyle, 41 | useColorfulLable: useColorfulLabel 42 | ) 43 | } 44 | 45 | // 设置面样式 46 | public func setFaceStyle( 47 | at index: Int, 48 | style: PathStyle? = nil, 49 | label: LabelStyle? = nil 50 | ) -> VectorMesh { 51 | guard index < faces.count else { return self } 52 | let style = FaceStyle( 53 | style: style, 54 | label: label 55 | ) 56 | self.faceStyleDict[index] = style 57 | return self 58 | } 59 | 60 | public func setFaceStyle( 61 | _ style: FaceStyle, 62 | for indices: Range? 63 | ) -> VectorMesh { 64 | let idxs = indices ?? 0 ..< faces.count 65 | for i in idxs { 66 | self.faceStyleDict[i] = style 67 | } 68 | return self 69 | } 70 | 71 | public func useColorfulLabel(_ value: Bool) -> Self { 72 | self.useColorfulLabel = value 73 | return self 74 | } 75 | 76 | // MARK: - modifier 77 | public func show(_ option: DisplayOptions) -> Self { 78 | self.displayOptions = option 79 | return self 80 | } 81 | 82 | public func log(_ message: Any..., level: Logger.Log.Level = .info) -> Self { 83 | self.logging(message, level: level) 84 | return self 85 | } 86 | 87 | func getMeshFaces() -> [VectorTriangleRenderElement] { 88 | faces.enumerated().map { (i, triangle) in 89 | createFace( 90 | triangle: triangle, 91 | faceIndex: i 92 | ) 93 | } 94 | } 95 | 96 | func createFace( 97 | triangle: VectorTriangle, 98 | faceIndex: Int 99 | ) -> VectorTriangleRenderElement { 100 | let customStyle = faceStyleDict[faceIndex] 101 | 102 | var labelString: String? 103 | if let faceLabel = customStyle?.label?.text { 104 | switch faceLabel { 105 | case .string(let string): 106 | labelString = string 107 | case .coordinate: 108 | // 计算三角形的中心点 109 | let center = (triangle.segment.start + triangle.segment.end + triangle.vertex) / 3.0 110 | labelString = "\(center)" 111 | case .index: 112 | labelString = "\(faceIndex)" 113 | case .orientation: 114 | // 使用 VectorTriangle 的 orientationSymbol 属性 115 | labelString = triangle.orientationSymbol 116 | } 117 | } 118 | 119 | let textColor: AppColor? = if useColorfulLabel { customStyle?.style?.color ?? self.color } else { nil } 120 | var label: TextElement? 121 | if let labelString { 122 | if let labelStyle = customStyle?.label?.style { 123 | label = TextElement(text: labelString, style: labelStyle) 124 | } else { 125 | label = TextElement( 126 | text: labelString, 127 | defaultStyle: labelStyle, 128 | location: customStyle?.label?.location ?? .center, 129 | textColor: textColor 130 | ) 131 | } 132 | } 133 | 134 | return VectorTriangleRenderElement( 135 | triangle: triangle, 136 | transform: transform, 137 | style: getFaceRenderStyle(style: customStyle?.style), 138 | label: label 139 | ) 140 | } 141 | } 142 | 143 | // MARK: - Transformable, DebugRenderable 144 | extension VectorMesh: DebugRenderable { 145 | public var debugBounds: CGRect? { 146 | // 计算所有三角形的边界框 147 | var bounds: CGRect? 148 | 149 | for face in faces { 150 | // 使用 VectorTriangle 的 bounds 属性 151 | let faceBounds = face.bounds 152 | if !faceBounds.isEmpty { 153 | if let currentBounds = bounds { 154 | bounds = currentBounds.union(faceBounds) 155 | } else { 156 | bounds = faceBounds 157 | } 158 | } 159 | } 160 | 161 | if let bounds = bounds { 162 | return bounds * transform 163 | } 164 | 165 | return nil 166 | } 167 | 168 | public func render(with transform: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 169 | let matrix = self.transform * transform 170 | // 只渲染面 171 | if displayOptions.contains(.face) { 172 | for face in faceElements { 173 | face.render(with: matrix, in: context, scale: scale, contextHeight: contextHeight) 174 | } 175 | } 176 | } 177 | } 178 | 179 | #Preview(traits: .fixedLayout(width: 400, height: 420)) { 180 | // 创建一些示例三角形 181 | let triangle1 = VectorTriangle( 182 | segment: .init( 183 | start: CGPoint(x: 50, y: 50), 184 | control1: CGPoint(x: 70, y: 30), 185 | control2: CGPoint(x: 90, y: 30), 186 | end: CGPoint(x: 110, y: 50) 187 | ), 188 | vertex: CGPoint(x: 80, y: 100) 189 | ) 190 | 191 | let triangle2 = VectorTriangle( 192 | segment: .init( 193 | start: CGPoint(x: 110, y: 50), 194 | control1: CGPoint(x: 130, y: 70), 195 | control2: CGPoint(x: 150, y: 70), 196 | end: CGPoint(x: 170, y: 50) 197 | ), 198 | vertex: CGPoint(x: 140, y: 100) 199 | ) 200 | 201 | DebugView(showOrigin: true) { 202 | VectorMesh(faces: [triangle1, triangle2]) 203 | .setFaceStyle(at: 0, style: .init(color: .blue.withAlphaComponent(0.2)), label: .index()) 204 | .setFaceStyle(at: 1, style: .init(color: .red.withAlphaComponent(0.2)), label: .coordinate()) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/render/base/segment/shapes/Arrow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrowRenderer.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/4. 6 | // 7 | 8 | import CoreGraphics 9 | import VisualUtils 10 | 11 | extension Arrow { 12 | 13 | public enum Direction: Sendable { 14 | case normal 15 | case reverse 16 | case double // double headed 17 | } 18 | 19 | public struct Tip: Sendable { 20 | public enum Shape: Sendable { 21 | case line // current implementation 22 | case triangle // draw a triangle and an line 23 | case topTriangle // top part of the triangle 24 | case bottomTriangle // bottom part of the triangle 25 | } 26 | public enum Anchor: Sendable { 27 | case midRight 28 | case midLeft 29 | } 30 | public var tip: CGPoint 31 | public var topLeft: CGPoint 32 | public var bottomLeft: CGPoint 33 | public var shape: Shape 34 | } 35 | 36 | public enum Style: Sendable { 37 | case normal 38 | case double(spacing: Double) 39 | } 40 | } 41 | 42 | public struct Arrow: Sendable { 43 | public let direction: Direction 44 | public let style: Style 45 | public let tip: Tip 46 | 47 | public init( 48 | direction: Direction = .normal, 49 | style: Style = .normal, 50 | tip: Tip = Tip() 51 | ) { 52 | self.direction = direction 53 | self.style = style 54 | self.tip = tip 55 | } 56 | } 57 | 58 | extension Arrow { 59 | public static let normal = Arrow(direction: .normal, style: .normal, tip: .init(shape: .triangle)) 60 | public static let doubleArrow = Arrow(direction: .normal, style: .double(spacing: 4), tip: .init(shape: .bottomTriangle)) 61 | } 62 | 63 | extension Arrow: SegmentRenderer { 64 | 65 | public func getBezierPath(start: CGPoint, end: CGPoint) -> AppBezierPath { 66 | switch self.style { 67 | case .normal: 68 | return getArrowPath(start: start, end: end) 69 | case .double(let spacing): 70 | let seg = Segment(start: start, end: end) 71 | let top = seg.offseting(distance: spacing/2) 72 | let btm = seg.offseting(distance: -spacing/2) 73 | let path = AppBezierPath() 74 | path.append(getArrowPath(start: top.start, end: top.end)) 75 | path.append(getArrowPath(start: btm.end, end: btm.start)) 76 | return path 77 | } 78 | } 79 | 80 | func getArrowPath(start: CGPoint, end: CGPoint) -> AppBezierPath { 81 | let angle = (end - start).angle 82 | let startTransform = Matrix2D(rotationAngle: angle + .pi) * Matrix2D(translation: start) 83 | let endTransform = Matrix2D(rotationAngle: angle) * Matrix2D(translation: end) 84 | let startTip = tip * startTransform 85 | let endTip = tip * endTransform 86 | 87 | let path = AppBezierPath() 88 | if direction == .normal || direction == .double { // has end 89 | let tip = endTip 90 | switch tip.shape { 91 | case .line: 92 | path.move(to: tip.topLeft) 93 | path.addLine(to: tip.tip) 94 | path.addLine(to: tip.bottomLeft) 95 | path.move(to: tip.tip) 96 | case .triangle: 97 | path.move(to: tip.middleLeft) 98 | path.addLine(to: tip.topLeft) 99 | path.addLine(to: tip.tip) 100 | path.addLine(to: tip.bottomLeft) 101 | path.close() 102 | path.move(to: tip.middleLeft) 103 | case .topTriangle: 104 | path.move(to: tip.middleLeft) 105 | path.addLine(to: tip.topLeft) 106 | path.addLine(to: tip.tip) 107 | path.close() 108 | path.move(to: tip.middleLeft) 109 | case .bottomTriangle: 110 | path.move(to: tip.middleLeft) 111 | path.addLine(to: tip.bottomLeft) 112 | path.addLine(to: tip.tip) 113 | path.close() 114 | path.move(to: tip.middleLeft) 115 | } 116 | } else { 117 | path.move(to: endTip.tip) 118 | } 119 | 120 | if direction == .reverse || direction == .double { // has start 121 | let tip = startTip 122 | switch tip.shape { 123 | case .line: 124 | path.addLine(to: tip.tip) 125 | 126 | path.move(to: tip.topLeft) 127 | path.addLine(to: tip.tip) 128 | path.addLine(to: tip.bottomLeft) 129 | case .triangle: 130 | path.addLine(to: tip.middleLeft) 131 | 132 | path.move(to: tip.middleLeft) 133 | path.addLine(to: tip.topLeft) 134 | path.addLine(to: tip.tip) 135 | path.addLine(to: tip.bottomLeft) 136 | path.close() 137 | 138 | case .topTriangle: 139 | path.addLine(to: tip.middleLeft) 140 | 141 | path.move(to: tip.middleLeft) 142 | path.addLine(to: tip.topLeft) 143 | path.addLine(to: tip.tip) 144 | path.close() 145 | case .bottomTriangle: 146 | path.addLine(to: tip.middleLeft) 147 | 148 | path.move(to: tip.middleLeft) 149 | path.addLine(to: tip.bottomLeft) 150 | path.addLine(to: tip.tip) 151 | path.close() 152 | } 153 | } else { 154 | path.addLine(to: startTip.tip) 155 | } 156 | return path 157 | } 158 | } 159 | 160 | extension Arrow.Tip { 161 | 162 | public var middleLeft: CGPoint { 163 | (topLeft + bottomLeft) / 2.0 164 | } 165 | 166 | public var length: Double { 167 | (middleLeft - tip).length 168 | } 169 | 170 | public init(angle: Double = .pi / 8, length: Double = 8, shape: Shape = .triangle, anchor: Anchor = .midRight) { 171 | let width = length 172 | let height = 2 * length * sin(angle) 173 | self.init(width: width, height: height, shape: shape, anchor: anchor) 174 | } 175 | 176 | public init(width: Double, height: Double, shape: Shape, anchor: Anchor = .midRight) { 177 | switch anchor { 178 | case .midRight: 179 | self.tip = .zero 180 | self.topLeft = .init(x: -width, y: -height/2) 181 | self.bottomLeft = .init(x: -width, y: height/2) 182 | case .midLeft: 183 | self.tip = .init(x: width, y: 0) 184 | self.topLeft = .init(x: 0, y: -height/2) 185 | self.bottomLeft = .init(x: 0, y: height/2) 186 | } 187 | self.shape = shape 188 | } 189 | } 190 | 191 | func *(lhs: Arrow.Tip, rhs: Matrix2D) -> Arrow.Tip { 192 | Arrow.Tip( 193 | tip: lhs.tip * rhs, 194 | topLeft: lhs.topLeft * rhs, 195 | bottomLeft: lhs.bottomLeft * rhs, 196 | shape: lhs.shape 197 | ) 198 | } 199 | -------------------------------------------------------------------------------- /readme-cn.md: -------------------------------------------------------------------------------- 1 | # VisualDebugger 2 | 3 | 最优雅、最简单的方式在源文件中可视化您的数据。VisualDebugger 是一个强大的 Swift 库,帮助您直接在代码中通过可视化表示调试几何数据结构。 4 | 5 | ## 功能特点 6 | 7 | - [x] 支持多种坐标系统(yUp, yDown) 8 | - [x] 支持可视化调试Mesh网格结构 9 | - [x] 支持可视化调试多边形(Polygon)及样式定制 10 | - [x] 支持可视化调试贝塞尔路径 11 | - [x] 支持可视化调试线条、点和多边形 12 | - [x] 支持iOS和macOS平台 13 | - [x] 灵活的样式定制系统 14 | - [x] 详细的坐标轴显示和标注 15 | - [x] 缩放和平移功能 16 | - [x] 自定义顶点和边缘样式 17 | - [x] 面的颜色和透明度控制 18 | 19 | ## 系统要求 20 | 21 | - iOS 17.0+ | macOS 15+ 22 | - Swift 6.0+ 23 | - Xcode 16+ 24 | 25 | ## 安装方法 26 | 27 | ### Swift Package Manager 28 | 29 | 您可以使用[Swift Package Manager](https://swift.org/package-manager)安装`VisualDebugger`,只需将其添加到您的`Package.swift`文件中: 30 | 31 | ```swift 32 | import PackageDescription 33 | 34 | let package = Package( 35 | name: "YOUR_PROJECT_NAME", 36 | dependencies: [ 37 | .package(url: "https://github.com/chenyunguiMilook/VisualDebugger.git", from: "3.0.0") 38 | ], 39 | targets: [ 40 | .target( 41 | name: "YOUR_TARGET_NAME", 42 | dependencies: ["VisualDebugger"]), 43 | ] 44 | ) 45 | ``` 46 | 47 | ### CocoaPods 48 | 49 | 将以下行添加到您的 Podfile 中: 50 | 51 | ```ruby 52 | pod 'VisualDebugger' 53 | ``` 54 | 55 | 然后运行 `pod install`。 56 | 57 | ## 核心组件 58 | 59 | ### DebugView 60 | 61 | 显示调试可视化的主视图。您可以通过各种参数自定义其外观和行为: 62 | 63 | ```swift 64 | DebugView( 65 | minWidth: Double = 250, 66 | numSegments: Int = 5, 67 | showOrigin: Bool = false, 68 | showCoordinate: Bool = true, 69 | coordinateSystem: CoordinateSystem2D = .yDown, 70 | coordinateStyle: CoordinateStyle = .default, 71 | elements: [any DebugRenderable] 72 | ) 73 | ``` 74 | 75 | ### 支持的调试器 76 | 77 | - **Polygon**:使用可自定义样式可视化点集合 78 | - **Mesh**:调试具有顶点、边和面的网格结构 79 | - **Line**:使用各种样式可视化线段 80 | - **Dot**:使用自定义形状和标签显示点 81 | - **Polygon**:使用样式选项调试多边形形状 82 | - **VectorMesh**:具有矢量数据的高级网格可视化 83 | 84 | ## 使用示例 85 | 86 | ### 调试多边形 87 | 88 | ```swift 89 | #Preview(traits: .fixedLayout(width: 400, height: 420)) { 90 | DebugView { 91 | Polygon([ 92 | .init(x: 40, y: 10), 93 | .init(x: 10, y: 23), 94 | .init(x: 23, y: 67) 95 | ], vertexShape: .index) 96 | .setVertexStyle(at: 0, shape: .shape(Circle(radius: 2)), label: "Corner") 97 | .setVertexStyle(at: 1, style: .init(color: .red), label: .coordinate()) 98 | .setEdgeStyle(at: 2, shape: .arrow(.doubleArrow), style: .init(color: .red, mode: .fill)) 99 | .show([.vertex, .edge]) 100 | } 101 | .coordinateVisible(true) 102 | .coordinateStyle(.default) 103 | .coordinateSystem(.yDown) 104 | //.zoom(1.5, aroundCenter: .init(x: 10, y: 23)) 105 | } 106 | ``` 107 | 108 | ![调试多边形](./Images/debug_points.png) 109 | 110 | ### 调试网格结构 111 | 112 | ```swift 113 | #Preview(traits: .fixedLayout(width: 400, height: 420)) { 114 | let vertices = [ 115 | CGPoint(x: 50, y: 50), 116 | CGPoint(x: 150, y: 50), 117 | CGPoint(x: 100, y: 150), 118 | CGPoint(x: 200, y: 150) 119 | ] 120 | 121 | let faces = [ 122 | Mesh.Face(0, 1, 2), 123 | Mesh.Face(1, 3, 2) 124 | ] 125 | 126 | DebugView(showOrigin: true) { 127 | Mesh(vertices, faces: faces) 128 | .setVertexStyle(at: 0, shape: .index, label: .coordinate(at: .top)) 129 | .setVertexStyle(at: 1, style: .init(color: .red), label: "顶点1") 130 | .setEdgeStyle(for: .init(org: 2, dst: 1), style: .init(color: .green)) 131 | .setFaceStyle(at: 0, color: .blue, alpha: 0.2) 132 | } 133 | } 134 | ``` 135 | 136 | ![调试网格](./Images/debug_mesh.png) 137 | 138 | ### 自定义坐标系统 139 | 140 | 您可以自定义坐标系统显示: 141 | 142 | ```swift 143 | DebugView { 144 | // 您的调试元素 145 | } 146 | .coordinateVisible(true) // 显示/隐藏坐标系统 147 | .coordinateStyle(.init( 148 | axisColor: .blue, 149 | gridColor: .gray.opacity(0.3), 150 | textColor: .black, 151 | axisWidth: 1.5, 152 | gridWidth: 0.5, 153 | fontSize: 10 154 | )) 155 | .coordinateSystem(.yUp) // 在 .yUp 和 .yDown 之间切换 156 | ``` 157 | 158 | ### 样式选项 159 | 160 | VisualDebugger 为顶点、边和面提供了广泛的样式选项: 161 | 162 | ```swift 163 | // 顶点样式 164 | .setVertexStyle( 165 | at: index, // 顶点索引 166 | shape: .circle, // 形状 (.circle, .square, .index 等) 167 | style: .init( // 样式属性 168 | color: .red, 169 | mode: .stroke, 170 | lineWidth: 1.5 171 | ), 172 | label: "自定义标签" // 可选标签 173 | ) 174 | 175 | // 边缘样式 176 | .setEdgeStyle( 177 | at: index, // 边缘索引 178 | shape: .arrow(.line), // 形状 (.line, .arrow 179 | style: .init( // 样式属性 180 | color: .blue, 181 | mode: .fill, 182 | lineWidth: 1.0 183 | ) 184 | ) 185 | 186 | // 面样式 187 | .setFaceStyle( 188 | at: index, // 面索引 189 | color: .green, // 填充颜色 190 | alpha: 0.3 // 透明度 191 | ) 192 | ``` 193 | 194 | ## 高级用法 195 | 196 | ### 捕获调试视图 197 | 198 | 您可以将调试视图捕获为图像,用于文档或共享: 199 | 200 | ```swift 201 | let image = DebugCapture.captureToImage { 202 | DebugView { 203 | // 您的调试元素 204 | } 205 | } 206 | ``` 207 | 208 | ### 创建程序执行过程的快照 209 | 210 | VisualDebugger 允许您在程序执行过程中创建快照,以可视化整个过程: 211 | 212 | ```swift 213 | // 创建捕获实例 214 | DebugCapture.shared = DebugCapture(context: context, folder: folder) 215 | 216 | // 在执行过程中,在不同阶段捕获快照 217 | func processAlgorithm() { 218 | // 初始状态 219 | let points = [CGPoint(x: 10, y: 20), CGPoint(x: 30, y: 40)] 220 | DebugCapture.shared?.captureElements("初始状态") { 221 | Polygon(points, vertexShape: .circle) 222 | } 223 | 224 | // 第一次转换后 225 | let transformedPoints = transform(points) 226 | DebugCapture.shared?.captureElements("转换后") { 227 | Polygon(transformedPoints, vertexShape: .circle) 228 | } 229 | 230 | // 最终结果 231 | let result = finalProcess(transformedPoints) 232 | DebugCapture.shared?.captureElements("最终结果") { 233 | Polygon(result, vertexShape: .circle) 234 | } 235 | 236 | // 生成整个过程的可视化 237 | DebugCapture.shared?.output() 238 | } 239 | ``` 240 | 241 | 这种方法对于调试复杂算法或可视化逐步过程非常有价值。 242 | 243 | ### 使用 DebugContext 在调试器中快速可视化 244 | 245 | 您可以直接创建 `DebugContext` 来在调试过程中可视化您的数据结构。这在您想要在 Xcode 调试器中检查几何数据时特别有用: 246 | 247 | ```swift 248 | // 创建包含您数据的调试上下文 - 必须在初始化时提供元素 249 | // 以便正确计算视图大小 250 | let vertices = [CGPoint(x: 10, y: 20), CGPoint(x: 30, y: 40), CGPoint(x: 50, y: 10)] 251 | let debugContext = DebugContext(elements: [Polygon(vertices, vertexShape: .circle)]) 252 | 253 | // 或者,您可以设置其他参数 254 | let debugContext = DebugContext( 255 | minWidth: 300, 256 | numSegments: 8, 257 | showOrigin: true, 258 | showCoordinate: true, 259 | coordinateSystem: .yDown, 260 | coordinateStyle: .default, 261 | elements: [Polygon(vertices, vertexShape: .circle)] 262 | ) 263 | 264 | // 在调试模式下,您可以使用 Quick Look 来可视化数据 265 | // 只需将鼠标悬停在 debugContext 变量上并点击眼睛图标 266 | // 或使用 debugQuickLookObject() 方法 267 | ``` 268 | 269 | 这种方法允许您直接在调试器中快速可视化复杂的几何结构,而无需在 UI 中设置完整的 DebugView。 270 | 271 | ### 自定义调试元素 272 | 273 | 您可以通过遵循 `DebugRenderable` 协议创建自定义调试元素: 274 | 275 | ```swift 276 | struct CustomDebugElement: DebugRenderable { 277 | // 实现细节 278 | } 279 | ``` 280 | 281 | ## 许可证 282 | 283 | VisualDebugger 使用 MIT 许可证。详情请参阅 LICENSE 文件。 284 | -------------------------------------------------------------------------------- /Sources/VisualDebugger/debuggers/Ray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ray.swift 3 | // VisualDebugger 4 | // 5 | // Created by chenyungui on 2025/3/11. 6 | // 7 | 8 | import CoreGraphics 9 | #if canImport(UIKit) 10 | import UIKit 11 | #elseif canImport(AppKit) 12 | import AppKit 13 | #endif 14 | import VisualUtils 15 | 16 | public typealias VRay = Ray 17 | 18 | public final class Ray: SegmentDebugger { 19 | 20 | public let start: CGPoint 21 | 22 | public var angle: CGFloat 23 | public var length: CGFloat 24 | 25 | public var end: CGPoint { 26 | CGPoint( 27 | x: start.x + cos(angle) * length, 28 | y: start.y + sin(angle) * length 29 | ) 30 | } 31 | 32 | var center: CGPoint { (start + end) / 2.0 } 33 | 34 | public lazy var vertices: [Vertex] = getVertices(from: [start, end]) 35 | public lazy var edge: MeshEdge = { 36 | createEdge(start: start, end: end, edgeIndex: 0, startIndex: 0, endIndex: 1) 37 | }() 38 | 39 | // 通过起点、角度和长度初始化 40 | public init( 41 | start: CGPoint, 42 | angle: Double = 0, 43 | length: Double = 50, 44 | name: String? = nil, 45 | transform: Matrix2D = .identity, 46 | color: AppColor = .yellow, 47 | vertexShape: VertexShape = .shape(Circle(radius: 2)), 48 | edgeShape: EdgeShape = .arrow(Arrow()), 49 | edgeStyle: EdgeStyle? = nil, 50 | labelStyle: TextRenderStyle = .nameLabel, 51 | useColorfulLabel: Bool = false 52 | ) { 53 | self.start = start 54 | self.angle = angle 55 | self.length = length 56 | 57 | super.init( 58 | name: name, 59 | transform: transform, 60 | color: color, 61 | vertexShape: vertexShape, 62 | edgeShape: edgeShape, 63 | labelStyle: labelStyle, 64 | useColorfulLable: useColorfulLabel 65 | ) 66 | self.vertexStyleDict[1] = .init(shape: .shape(.empty()), style: nil, label: nil) 67 | } 68 | 69 | // 通过起点和终点初始化 70 | public convenience init( 71 | start: CGPoint, 72 | end: CGPoint, 73 | name: String? = nil, 74 | transform: Matrix2D = .identity, 75 | color: AppColor = .yellow, 76 | vertexShape: VertexShape = .shape(Circle(radius: 2)), 77 | edgeShape: EdgeShape = .arrow(Arrow()), 78 | edgeStyle: EdgeStyle? = nil, 79 | labelStyle: TextRenderStyle = .nameLabel, 80 | useColorfulLabel: Bool = false 81 | ) { 82 | self.init( 83 | start: start, 84 | angle: (end - start).angle, 85 | length: (end - start).length, 86 | name: name, 87 | transform: transform, 88 | color: color, 89 | vertexShape: vertexShape, 90 | edgeShape: edgeShape, 91 | edgeStyle: edgeStyle, 92 | labelStyle: labelStyle, 93 | useColorfulLabel: useColorfulLabel 94 | ) 95 | } 96 | 97 | public convenience init( 98 | start: CGPoint, 99 | direction: CGPoint, 100 | length: Double = 10, 101 | name: String? = nil, 102 | transform: Matrix2D = .identity, 103 | color: AppColor = .yellow, 104 | vertexShape: VertexShape = .shape(Circle(radius: 2)), 105 | edgeShape: EdgeShape = .arrow(Arrow()), 106 | edgeStyle: EdgeStyle? = nil, 107 | labelStyle: TextRenderStyle = .nameLabel, 108 | useColorfulLabel: Bool = false 109 | ) { 110 | self.init( 111 | start: start, 112 | angle: direction.angle, 113 | length: length, 114 | name: name, 115 | transform: transform, 116 | color: color, 117 | vertexShape: vertexShape, 118 | edgeShape: edgeShape, 119 | edgeStyle: edgeStyle, 120 | labelStyle: labelStyle, 121 | useColorfulLabel: useColorfulLabel 122 | ) 123 | } 124 | } 125 | 126 | extension Ray: DebugRenderable { 127 | public var debugBounds: CGRect? { 128 | let minX = min(start.x, end.x) 129 | let minY = min(start.y, end.y) 130 | let maxX = max(start.x, end.x) 131 | let maxY = max(start.y, end.y) 132 | let rect = CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) 133 | return rect * transform 134 | } 135 | 136 | public func render(with transform: Matrix2D, in context: CGContext, scale: CGFloat, contextHeight: Int?) { 137 | let matrix = self.transform * transform 138 | if displayOptions.contains(.edge) { 139 | edge.render(with: matrix, in: context, scale: scale, contextHeight: contextHeight) 140 | } 141 | if displayOptions.contains(.vertex) { 142 | for vertex in self.vertices { 143 | vertex.render(with: matrix, in: context, scale: scale, contextHeight: contextHeight) 144 | } 145 | } 146 | } 147 | } 148 | 149 | extension Ray { 150 | // 设置边样式 151 | @discardableResult 152 | public func setEdgeStyle( 153 | shape: EdgeShape? = nil, 154 | style: PathStyle? = nil, 155 | label: LabelStyle? = nil, 156 | offset: Double? = nil 157 | ) -> Ray { 158 | self.edgeStyleDict[0] = EdgeStyle( 159 | shape: shape, 160 | style: style, 161 | label: label, 162 | offset: offset 163 | ) 164 | return self 165 | } 166 | 167 | // 设置起点样式 168 | @discardableResult 169 | public func setStartStyle( 170 | shape: VertexShape? = nil, 171 | style: PathStyle? = nil, 172 | label: LabelStyle? = nil 173 | ) -> Ray { 174 | let style = VertexStyle(shape: shape, style: style, label: label) 175 | self.vertexStyleDict[0] = style 176 | return self 177 | } 178 | 179 | // 设置终点样式 180 | @discardableResult 181 | public func setEndStyle( 182 | shape: VertexShape? = nil, 183 | style: PathStyle? = nil, 184 | label: LabelStyle? = nil 185 | ) -> Ray { 186 | let style = VertexStyle(shape: shape, style: style, label: label) 187 | self.vertexStyleDict[1] = style 188 | return self 189 | } 190 | 191 | public func setStartStyle(_ style: VertexStyle) -> Ray { 192 | self.vertexStyleDict[0] = style 193 | return self 194 | } 195 | 196 | public func setEndStyle(_ style: VertexStyle) -> Ray { 197 | self.vertexStyleDict[1] = style 198 | return self 199 | } 200 | 201 | public func useColorfulLabel(_ value: Bool) -> Ray { 202 | self.useColorfulLabel = value 203 | return self 204 | } 205 | 206 | public func setAngle(_ angle: Double) -> Self { 207 | self.angle = angle 208 | return self 209 | } 210 | 211 | public func setLength(_ length: Double) -> Self { 212 | self.length = length 213 | return self 214 | } 215 | 216 | // 显示选项 217 | public func show(_ option: DisplayOptions) -> Self { 218 | self.displayOptions = option 219 | return self 220 | } 221 | 222 | public func log(_ message: Any..., level: Logger.Log.Level = .info) -> Self { 223 | self.logging(message, level: level) 224 | return self 225 | } 226 | } 227 | 228 | #Preview(traits: .fixedLayout(width: 400, height: 420)) { 229 | DebugView { 230 | // 使用两点初始化 231 | Ray( 232 | start: .init(x: 50, y: 50), 233 | end: .init(x: 250, y: 150) 234 | ) 235 | .setLength(100) 236 | 237 | // 使用角度和长度初始化 238 | Ray( 239 | start: .init(x: 50, y: 200), 240 | angle: .pi / 4, // 45度 241 | length: 200, 242 | color: .green 243 | ) 244 | } 245 | .coordinateVisible(true) 246 | .coordinateStyle(.default) 247 | .coordinateSystem(.yDown) 248 | } 249 | -------------------------------------------------------------------------------- /VisualDebugger.xcworkspace/Playground.playground/Pages/TestMixDebugging.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import Foundation 4 | import VisualDebugger 5 | import UIKit 6 | 7 | let points: [CGPoint] = [CGPoint(x: 236.315262, y: 221.128799), 8 | CGPoint(x: 271.792175, y: 219.336243), 9 | CGPoint(x: 317.943420, y: 216.807373), 10 | CGPoint(x: 301.653198, y: 217.774872), 11 | CGPoint(x: 362.722015, y: 222.526703), 12 | CGPoint(x: 372.855133, y: 222.843811), 13 | CGPoint(x: 450.280823, y: 223.461029), 14 | CGPoint(x: 388.998169, y: 224.491913), 15 | CGPoint(x: 376.583038, y: 225.154373), 16 | CGPoint(x: 190.459061, y: 269.911133), 17 | CGPoint(x: 265.240875, y: 258.504761), 18 | CGPoint(x: 279.413666, y: 242.127243), 19 | CGPoint(x: 377.691071, y: 258.358459), 20 | CGPoint(x: 447.714203, y: 255.365692), 21 | CGPoint(x: 228.405029, y: 311.555145), 22 | CGPoint(x: 247.907608, y: 284.946716), 23 | CGPoint(x: 264.411774, y: 273.199280), 24 | CGPoint(x: 290.534760, y: 273.621185), 25 | CGPoint(x: 433.154175, y: 280.367981), 26 | CGPoint(x: 20.480394, y: 61.291805), 27 | CGPoint(x: 293.516724, y: 328.177246), 28 | CGPoint(x: 348.300507, y: 330.697388), 29 | CGPoint(x: 323.043976, y: 331.119324), 30 | CGPoint(x: 394.381317, y: 318.468536), 31 | CGPoint(x: 416.265686, y: 304.649048), 32 | CGPoint(x: 270.722992, y: 325.538544), 33 | CGPoint(x: 248.273193, y: 252.930283), 34 | CGPoint(x: 245.012054, y: 269.199707), 35 | CGPoint(x: 311.913239, y: 247.675293), 36 | CGPoint(x: 290.467468, y: 260.265686), 37 | CGPoint(x: 326.251251, y: 253.524338), 38 | CGPoint(x: 360.763336, y: 232.221695), 39 | CGPoint(x: 372.988800, y: 234.824509), 40 | CGPoint(x: 384.378387, y: 247.467117), 41 | CGPoint(x: 375.542450, y: 236.219940), 42 | CGPoint(x: 387.565674, y: 236.316925), 43 | CGPoint(x: 278.710693, y: 292.202271), 44 | CGPoint(x: 182.525757, y: 223.846619), 45 | CGPoint(x: 341.155212, y: 218.128281), 46 | CGPoint(x: 333.772247, y: 242.804764), 47 | CGPoint(x: 337.595886, y: 230.748749), 48 | CGPoint(x: 369.934631, y: 327.961090), 49 | CGPoint(x: 365.907471, y: 245.374313), 50 | CGPoint(x: 286.910767, y: 218.705490), 51 | CGPoint(x: 262.252869, y: 229.713394), 52 | CGPoint(x: 271.902985, y: 271.791046), 53 | CGPoint(x: 278.283020, y: 285.776703), 54 | CGPoint(x: 283.128265, y: 272.345276), 55 | CGPoint(x: 283.073334, y: 261.447021), 56 | CGPoint(x: 272.541077, y: 260.481995), 57 | CGPoint(x: 278.810242, y: 248.220428), 58 | CGPoint(x: 319.816071, y: 306.920563), 59 | CGPoint(x: 337.275879, y: 292.110870), 60 | CGPoint(x: 351.853455, y: 285.382935), 61 | CGPoint(x: 374.600830, y: 246.662735), 62 | CGPoint(x: 375.911804, y: 247.287979), 63 | CGPoint(x: 316.922058, y: 285.225098), 64 | CGPoint(x: 241.618668, y: 242.137863), 65 | CGPoint(x: 237.609879, y: 248.795593), 66 | CGPoint(x: 233.538284, y: 269.108093), 67 | CGPoint(x: 237.469086, y: 290.485077), 68 | CGPoint(x: 492.890991, y: 541.670471), 69 | CGPoint(x: 193.640961, y: 180.704849), 70 | CGPoint(x: 267.299377, y: 184.018936), 71 | CGPoint(x: 282.405365, y: 202.202026), 72 | CGPoint(x: 378.769958, y: 190.781464), 73 | CGPoint(x: 447.826691, y: 194.333237), 74 | CGPoint(x: 233.576691, y: 144.816727), 75 | CGPoint(x: 252.210907, y: 160.615326), 76 | CGPoint(x: 268.095825, y: 168.678650), 77 | CGPoint(x: 293.952942, y: 171.478271), 78 | CGPoint(x: 432.826660, y: 172.786987), 79 | CGPoint(x: 352.474121, y: 55.982208), 80 | CGPoint(x: 297.926880, y: 135.007675), 81 | CGPoint(x: 351.101044, y: 136.137955), 82 | CGPoint(x: 326.674561, y: 133.862625), 83 | CGPoint(x: 394.733124, y: 142.801926), 84 | CGPoint(x: 416.009949, y: 153.863220), 85 | CGPoint(x: 275.667786, y: 135.219009), 86 | CGPoint(x: 251.866608, y: 188.884048), 87 | CGPoint(x: 249.969009, y: 174.395081), 88 | CGPoint(x: 313.072296, y: 196.146957), 89 | CGPoint(x: 293.364075, y: 184.386139), 90 | CGPoint(x: 327.170837, y: 191.533325), 91 | CGPoint(x: 361.526306, y: 212.318924), 92 | CGPoint(x: 373.338928, y: 211.939545), 93 | CGPoint(x: 384.591522, y: 201.370331), 94 | CGPoint(x: 376.413086, y: 213.659775), 95 | CGPoint(x: 387.791046, y: 212.633560), 96 | CGPoint(x: 284.449677, y: 153.182678), 97 | CGPoint(x: 336.114166, y: 197.831482), 98 | CGPoint(x: 338.834290, y: 207.528534), 99 | CGPoint(x: 372.098633, y: 139.665710), 100 | CGPoint(x: 367.687775, y: 200.403168), 101 | CGPoint(x: 263.577301, y: 209.846024), 102 | CGPoint(x: 275.379761, y: 171.106400), 103 | CGPoint(x: 283.337250, y: 159.262009), 104 | CGPoint(x: 286.512054, y: 172.205780), 105 | CGPoint(x: 286.017242, y: 183.050964), 106 | CGPoint(x: 274.792999, y: 182.407852), 107 | CGPoint(x: 281.804016, y: 196.026520), 108 | CGPoint(x: 322.729889, y: 151.925079), 109 | CGPoint(x: 339.135925, y: 163.835632), 110 | CGPoint(x: 354.106384, y: 168.748596), 111 | CGPoint(x: 375.310516, y: 201.252090), 112 | CGPoint(x: 377.141998, y: 202.202118), 113 | CGPoint(x: 319.193329, y: 168.119003), 114 | CGPoint(x: 245.195389, y: 199.576675), 115 | CGPoint(x: 241.196243, y: 192.418304), 116 | CGPoint(x: 238.813126, y: 174.221817), 117 | CGPoint(x: 241.866302, y: 155.455566), 118 | CGPoint(x: 165.524597, y: 235.124481),] 119 | 120 | 121 | //points.debugView(of: [.big]) 122 | 123 | let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100)) 124 | let path2 = UIBezierPath.init(rect: CGRect.init(x: 0, y: 0, width: 40, height: 40)) 125 | [path, path2].debugView 126 | 127 | //debug([points, path, path2]) 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | --------------------------------------------------------------------------------