├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------