mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Merge commit '5b013073bf'
# Conflicts: # CLAUDE.md # MODULE.bazel.lock # submodules/TgVoipWebrtc/tgcalls # third-party/td/build-td-bazel.sh # third-party/webrtc/webrtc
This commit is contained in:
commit
8dc06f48ce
1570 changed files with 133567 additions and 34276 deletions
17
third-party/Swift2D/BUILD
vendored
Normal file
17
third-party/Swift2D/BUILD
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "Swift2D",
|
||||
module_name = "Swift2D",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-suppress-warnings",
|
||||
],
|
||||
deps = [
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
19
third-party/Swift2D/Sources/Point+CoreGraphics.swift
vendored
Normal file
19
third-party/Swift2D/Sources/Point+CoreGraphics.swift
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
#else
|
||||
import Foundation
|
||||
#endif
|
||||
|
||||
public extension Point {
|
||||
/// Initialize a `Point` using the provided `CGPoint` values.
|
||||
init(_ point: CGPoint) {
|
||||
self.init(x: point.x, y: point.y)
|
||||
}
|
||||
}
|
||||
|
||||
public extension CGPoint {
|
||||
/// Initialize a `CPPoint` using the provided `Point` values.
|
||||
init(_ point: Point) {
|
||||
self.init(x: point.x, y: point.y)
|
||||
}
|
||||
}
|
||||
82
third-party/Swift2D/Sources/Point.swift
vendored
Normal file
82
third-party/Swift2D/Sources/Point.swift
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/// The representation of a single point in a two-dimensional plane.
|
||||
public struct Point: Hashable, Codable, Sendable, CustomStringConvertible {
|
||||
|
||||
public static let zero: Point = Point(x: 0, y: 0)
|
||||
public static let nan: Point = Point(x: Double.nan, y: Double.nan)
|
||||
public static let infinite: Point = Point(x: -Double.greatestFiniteMagnitude / 2, y: -Double.greatestFiniteMagnitude / 2)
|
||||
public static let null: Point = Point(x: Double.infinity, y: Double.infinity)
|
||||
|
||||
public let x: Double
|
||||
public let y: Double
|
||||
|
||||
public init(x: Double = 0.0, y: Double = 0.0) {
|
||||
self.x = x
|
||||
self.y = y
|
||||
}
|
||||
|
||||
public init(x: Float, y: Float) {
|
||||
self.x = Double(x)
|
||||
self.y = Double(y)
|
||||
}
|
||||
|
||||
public init(x: Int, y: Int) {
|
||||
self.x = Double(x)
|
||||
self.y = Double(y)
|
||||
}
|
||||
|
||||
public var description: String { "Point(x: \(x), y: \(y))" }
|
||||
public var isZero: Bool { self == .zero }
|
||||
public var isNaN: Bool { self == .nan }
|
||||
public var isInfinite: Bool { self == .infinite }
|
||||
public var isNull: Bool { self == .null }
|
||||
|
||||
/// Create a new instance maintaining the `y` value while using the provided `x` value.
|
||||
public func x(_ value: Double) -> Point {
|
||||
Point(x: value, y: y)
|
||||
}
|
||||
|
||||
/// Create a new instance maintaining the `x` value while using the provided `y` value.
|
||||
public func y(_ value: Double) -> Point {
|
||||
Point(x: x, y: value)
|
||||
}
|
||||
|
||||
/// The _mirror_ of the instance, rotated 180° around the provided `point`
|
||||
/// in a two-dimensional plane.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - point: The anchor to use in calculating the reflection.
|
||||
public func reflecting(around point: Point) -> Point {
|
||||
let x = (2 * point.x) - x
|
||||
let y = (2 * point.y) - y
|
||||
return Point(x: x, y: y)
|
||||
}
|
||||
|
||||
public static func == (lhs: Point, rhs: Point) -> Bool {
|
||||
if lhs.x.isNaN, rhs.x.isNaN, lhs.y.isNaN, rhs.y.isNaN {
|
||||
return true
|
||||
}
|
||||
|
||||
if lhs.x.isInfinite, rhs.x.isInfinite, lhs.y.isInfinite, rhs.y.isInfinite {
|
||||
return true
|
||||
}
|
||||
|
||||
return lhs.x == rhs.x && lhs.y == rhs.y
|
||||
}
|
||||
}
|
||||
|
||||
public extension Point {
|
||||
@available(*, deprecated, renamed: "x(_:)")
|
||||
func with(x value: Double) -> Point {
|
||||
x(value)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "y(_:)")
|
||||
func with(y value: Double) -> Point {
|
||||
y(value)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "reflecting(around:)")
|
||||
func reflection(using point: Point) -> Point {
|
||||
reflecting(around: point)
|
||||
}
|
||||
}
|
||||
32
third-party/Swift2D/Sources/Rect+CoreGraphics.swift
vendored
Normal file
32
third-party/Swift2D/Sources/Rect+CoreGraphics.swift
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
#else
|
||||
import Foundation
|
||||
#endif
|
||||
|
||||
public extension Rect {
|
||||
/// Initialize a `Rect` using the provided `CGRect` values.
|
||||
init(_ rect: CGRect) {
|
||||
self.init(
|
||||
origin: Point(rect.origin),
|
||||
size: Size(rect.size)
|
||||
)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "CGRect(_:)", message: "Use CGRect initializer directly")
|
||||
var cgRect: CGRect {
|
||||
CGRect(self)
|
||||
}
|
||||
}
|
||||
|
||||
public extension CGRect {
|
||||
/// Initialize a `CGRect` using the provided `Rect` values.
|
||||
init(_ rect: Rect) {
|
||||
self.init(
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
)
|
||||
}
|
||||
}
|
||||
251
third-party/Swift2D/Sources/Rect.swift
vendored
Normal file
251
third-party/Swift2D/Sources/Rect.swift
vendored
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
/// The location and dimensions of a rectangle.
|
||||
public struct Rect: Hashable, Codable, Sendable, CustomStringConvertible {
|
||||
|
||||
public static let zero: Rect = Rect(origin: .zero, size: .zero)
|
||||
public static let nan: Rect = Rect(origin: .nan, size: .nan)
|
||||
public static let infinite: Rect = Rect(origin: .infinite, size: .infinite)
|
||||
public static let null: Rect = Rect(origin: .null, size: .zero)
|
||||
|
||||
/// A point that specifies the coordinates of the rectangle’s origin.
|
||||
public let origin: Point
|
||||
/// A size that specifies the height and width of the rectangle.
|
||||
public let size: Size
|
||||
|
||||
public init(origin: Point = .zero, size: Size = .zero) {
|
||||
self.origin = origin
|
||||
self.size = size
|
||||
}
|
||||
|
||||
public init(x: Double, y: Double, width: Double, height: Double) {
|
||||
origin = Point(x: x, y: y)
|
||||
size = Size(width: width, height: height)
|
||||
}
|
||||
|
||||
public init(x: Float, y: Float, width: Float, height: Float) {
|
||||
origin = Point(x: x, y: y)
|
||||
size = Size(width: width, height: height)
|
||||
}
|
||||
|
||||
public init(x: Int, y: Int, width: Int, height: Int) {
|
||||
origin = Point(x: x, y: y)
|
||||
size = Size(width: width, height: height)
|
||||
}
|
||||
|
||||
public var description: String { "Rect(origin: \(origin), size: \(size))" }
|
||||
public var isZero: Bool { self == .zero }
|
||||
public var isNaN: Bool { self == .nan }
|
||||
public var isInfinite: Bool { self == .infinite }
|
||||
public var isNull: Bool { origin == .infinite || origin == .null }
|
||||
public var isEmpty: Bool { isNull || width == 0.0 || height == 0.0 }
|
||||
|
||||
/// The x-coordinate of the rectangle origin
|
||||
public var x: Double { origin.x }
|
||||
|
||||
/// The y-coordinate of the rectangle origin
|
||||
public var y: Double { origin.y }
|
||||
|
||||
/// The width of the rectangle.
|
||||
public var width: Double { size.width }
|
||||
|
||||
/// The height of the rectangle.
|
||||
public var height: Double { size.height }
|
||||
|
||||
/// The middle of the `Rect` both horizontally and vertically.
|
||||
public var center: Point { Point(x: midX, y: midY) }
|
||||
|
||||
/// The smallest value for the x-coordinate of the rectangle.
|
||||
public var minX: Double {
|
||||
if size.width < 0.0 {
|
||||
return origin.x - abs(size.width)
|
||||
}
|
||||
|
||||
return origin.x
|
||||
}
|
||||
|
||||
/// The x-coordinate that establishes the center of a rectangle.
|
||||
public var midX: Double { minX + size.widthRadius }
|
||||
|
||||
/// The largest value of the x-coordinate for the rectangle.
|
||||
public var maxX: Double {
|
||||
if size.width < 0.0 {
|
||||
return origin.x
|
||||
}
|
||||
|
||||
return origin.x + size.width
|
||||
}
|
||||
|
||||
/// The smallest value for the y-coordinate of the rectangle.
|
||||
public var minY: Double {
|
||||
if size.height < 0.0 {
|
||||
return origin.y - abs(size.height)
|
||||
}
|
||||
|
||||
return origin.y
|
||||
}
|
||||
|
||||
/// The y-coordinate that establishes the center of the rectangle.
|
||||
public var midY: Double { minY + size.heightRadius }
|
||||
|
||||
/// The largest value for the y-coordinate of the rectangle.
|
||||
public var maxY: Double {
|
||||
if size.height < 0.0 {
|
||||
return origin.y
|
||||
}
|
||||
|
||||
return origin.y + size.height
|
||||
}
|
||||
|
||||
/// Returns a rectangle with a positive width and height.
|
||||
public var standardized: Rect {
|
||||
guard !isNull else {
|
||||
return .null
|
||||
}
|
||||
|
||||
return Rect(x: minX, y: minY, width: abs(width), height: abs(height))
|
||||
}
|
||||
|
||||
public func origin(_ value: Point) -> Rect {
|
||||
Rect(origin: value, size: size)
|
||||
}
|
||||
|
||||
public func size(_ value: Size) -> Rect {
|
||||
Rect(origin: origin, size: value)
|
||||
}
|
||||
|
||||
public func contains(_ point: Point) -> Bool {
|
||||
guard !isNull else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard !isEmpty else {
|
||||
return false
|
||||
}
|
||||
|
||||
return (minX ..< maxX).contains(point.x) && (minY ..< maxY).contains(point.y)
|
||||
}
|
||||
|
||||
public func contains(_ rect: Rect) -> Bool {
|
||||
union(rect) == self
|
||||
}
|
||||
|
||||
public func intersects(_ rect: Rect) -> Bool {
|
||||
!intersection(rect).isNull
|
||||
}
|
||||
|
||||
public func intersection(_ rect: Rect) -> Rect {
|
||||
guard !isNull else {
|
||||
return .null
|
||||
}
|
||||
|
||||
guard !rect.isNull else {
|
||||
return .null
|
||||
}
|
||||
|
||||
let r1 = standardized
|
||||
let r1spanH = r1.minX ... r1.maxX
|
||||
let r1spanV = r1.minY ... r1.maxY
|
||||
|
||||
let r2 = rect.standardized
|
||||
let r2spanH = r2.minX ... r2.maxX
|
||||
let r2spanV = r2.minY ... r2.maxY
|
||||
|
||||
guard r1spanH.overlaps(r2spanH), r2spanV.overlaps(r2spanV) else {
|
||||
return .null
|
||||
}
|
||||
|
||||
let overlapH = r1spanH.clamped(to: r2spanH)
|
||||
let width: Double = if overlapH == r1spanH {
|
||||
r1.width
|
||||
} else if overlapH == r2spanH {
|
||||
r2.width
|
||||
} else {
|
||||
overlapH.upperBound - overlapH.lowerBound
|
||||
}
|
||||
|
||||
let overlapV = r1spanV.clamped(to: r2spanV)
|
||||
let height: Double = if overlapV == r1spanV {
|
||||
r1.height
|
||||
} else if overlapV == r2spanV {
|
||||
r2.height
|
||||
} else {
|
||||
overlapV.upperBound - overlapV.lowerBound
|
||||
}
|
||||
|
||||
return Rect(x: overlapH.lowerBound, y: overlapV.lowerBound, width: width, height: height)
|
||||
}
|
||||
|
||||
public func union(_ rect: Rect) -> Rect {
|
||||
guard !isNull else {
|
||||
return rect
|
||||
}
|
||||
|
||||
guard !rect.isNull else {
|
||||
return self
|
||||
}
|
||||
|
||||
let r1 = standardized
|
||||
let r2 = rect.standardized
|
||||
|
||||
let minX = min(r1.minX, r2.minX)
|
||||
let maxX = max(r1.maxX, r2.maxX)
|
||||
let minY = min(r1.minY, r2.minY)
|
||||
let maxY = max(r1.maxY, r2.maxY)
|
||||
|
||||
return Rect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
|
||||
}
|
||||
|
||||
public func offsetBy(dx: Double, dy: Double) -> Rect {
|
||||
guard !isNull else {
|
||||
return self
|
||||
}
|
||||
|
||||
let rect = standardized
|
||||
return Rect(
|
||||
origin: Point(
|
||||
x: rect.origin.x + dx,
|
||||
y: rect.origin.y + dy
|
||||
),
|
||||
size: rect.size
|
||||
)
|
||||
}
|
||||
|
||||
public func insetBy(dx: Double, dy: Double) -> Rect {
|
||||
guard !isNull else {
|
||||
return self
|
||||
}
|
||||
|
||||
let normalized = standardized
|
||||
let rect = Rect(
|
||||
origin: Point(
|
||||
x: normalized.x + dx,
|
||||
y: normalized.y + dy
|
||||
),
|
||||
size: Size(
|
||||
width: normalized.width - (dx * 2),
|
||||
height: normalized.height - (dy * 2)
|
||||
)
|
||||
)
|
||||
|
||||
guard rect.size.width >= 0, rect.size.height >= 0 else {
|
||||
return .null
|
||||
}
|
||||
|
||||
return rect
|
||||
}
|
||||
|
||||
public func expandedBy(dx: Double, dy: Double) -> Rect {
|
||||
insetBy(dx: -dx, dy: -dy)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Rect {
|
||||
@available(*, deprecated, renamed: "origin(_:)")
|
||||
func with(origin value: Point) -> Rect {
|
||||
Rect(origin: value, size: size)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "size(_:)")
|
||||
func with(size value: Size) -> Rect {
|
||||
Rect(origin: origin, size: value)
|
||||
}
|
||||
}
|
||||
19
third-party/Swift2D/Sources/Size+CoreGraphics.swift
vendored
Normal file
19
third-party/Swift2D/Sources/Size+CoreGraphics.swift
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
#else
|
||||
import Foundation
|
||||
#endif
|
||||
|
||||
public extension Size {
|
||||
/// Initialize a `Size` using the provided `CGSize` values.
|
||||
init(_ size: CGSize) {
|
||||
self.init(width: size.width, height: size.height)
|
||||
}
|
||||
}
|
||||
|
||||
public extension CGSize {
|
||||
/// Initialize a `CGSize` using the provided `Size` values.
|
||||
init(_ size: Size) {
|
||||
self.init(width: size.width, height: size.height)
|
||||
}
|
||||
}
|
||||
87
third-party/Swift2D/Sources/Size.swift
vendored
Normal file
87
third-party/Swift2D/Sources/Size.swift
vendored
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/// A representation of two-dimensional width and height values.
|
||||
public struct Size: Hashable, Codable, Sendable, CustomStringConvertible {
|
||||
|
||||
public static let zero: Size = Size(width: 0.0, height: 0.0)
|
||||
public static let nan: Size = Size(width: Double.nan, height: Double.nan)
|
||||
public static let infinite: Size = Size(width: Double.greatestFiniteMagnitude, height: Double.greatestFiniteMagnitude)
|
||||
|
||||
public let width: Double
|
||||
public let height: Double
|
||||
|
||||
public init(width: Double = 0.0, height: Double = 0.0) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
}
|
||||
|
||||
public init(width: Float, height: Float) {
|
||||
self.width = Double(width)
|
||||
self.height = Double(height)
|
||||
}
|
||||
|
||||
public init(width: Int, height: Int) {
|
||||
self.width = Double(width)
|
||||
self.height = Double(height)
|
||||
}
|
||||
|
||||
public var description: String { "Size(width: \(width), height: \(height))" }
|
||||
public var isZero: Bool { self == .zero }
|
||||
public var isNaN: Bool { self == .nan }
|
||||
public var isInfinite: Bool { self == .infinite }
|
||||
|
||||
/// The radius defined by the `width` dimension.
|
||||
public var widthRadius: Double { abs(width) / 2.0 }
|
||||
|
||||
/// The radius defined by the `height` dimension.
|
||||
public var heightRadius: Double { abs(height) / 2.0 }
|
||||
|
||||
/// The largest of the width and height radii.
|
||||
public var maxRadius: Double { max(widthRadius, heightRadius) }
|
||||
|
||||
/// The smallest of the width and height radii.
|
||||
public var minRadius: Double { min(widthRadius, heightRadius) }
|
||||
|
||||
/// The `Point` at which the `widthRadius` & `heightRadius` intersect.
|
||||
public var center: Point { Point(x: widthRadius, y: heightRadius) }
|
||||
|
||||
/// Create a new instance maintaining the `height` value while using the provided `width` value.
|
||||
public func width(_ value: Double) -> Size {
|
||||
Size(width: value, height: height)
|
||||
}
|
||||
|
||||
/// Create a new instance maintaining the `width` value while using the provided `height` value.
|
||||
public func height(_ value: Double) -> Size {
|
||||
Size(width: width, height: value)
|
||||
}
|
||||
|
||||
public static func == (lhs: Size, rhs: Size) -> Bool {
|
||||
if lhs.width.isNaN, rhs.width.isNaN, lhs.height.isNaN, rhs.height.isNaN {
|
||||
return true
|
||||
}
|
||||
|
||||
return lhs.width == rhs.width && lhs.height == rhs.height
|
||||
}
|
||||
}
|
||||
|
||||
public extension Size {
|
||||
@available(*, deprecated, renamed: "widthRadius")
|
||||
var xRadius: Double { abs(width) / 2.0 }
|
||||
|
||||
@available(*, deprecated, renamed: "heightRadius")
|
||||
var yRadius: Double { abs(height) / 2.0 }
|
||||
|
||||
@available(*, deprecated, renamed: "widthRadius")
|
||||
var horizontalRadius: Double { xRadius }
|
||||
|
||||
@available(*, deprecated, renamed: "heightRadius")
|
||||
var verticalRadius: Double { yRadius }
|
||||
|
||||
@available(*, deprecated, renamed: "width(_:)")
|
||||
func with(width value: Double) -> Size {
|
||||
width(value)
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "height(_:)")
|
||||
func with(height value: Double) -> Size {
|
||||
height(value)
|
||||
}
|
||||
}
|
||||
17
third-party/SwiftColor/BUILD
vendored
Normal file
17
third-party/SwiftColor/BUILD
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SwiftColor",
|
||||
module_name = "SwiftColor",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-suppress-warnings",
|
||||
],
|
||||
deps = [
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
20
third-party/SwiftColor/Sources/Clamping.swift
vendored
Normal file
20
third-party/SwiftColor/Sources/Clamping.swift
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
@propertyWrapper
|
||||
public struct Clamping<Value: Comparable> {
|
||||
var value: Value
|
||||
let range: ClosedRange<Value>
|
||||
|
||||
public init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
|
||||
precondition(range.contains(value))
|
||||
self.value = value
|
||||
self.range = range
|
||||
}
|
||||
|
||||
public var wrappedValue: Value {
|
||||
get {
|
||||
value
|
||||
}
|
||||
set(newValue) {
|
||||
value = min(max(range.lowerBound, newValue), range.upperBound)
|
||||
}
|
||||
}
|
||||
}
|
||||
3
third-party/SwiftColor/Sources/ColorSpace.swift
vendored
Normal file
3
third-party/SwiftColor/Sources/ColorSpace.swift
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
public enum ColorSpace: Equatable, Sendable {
|
||||
case rgba
|
||||
}
|
||||
24
third-party/SwiftColor/Sources/Pigment+AppKit.swift
vendored
Normal file
24
third-party/SwiftColor/Sources/Pigment+AppKit.swift
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import Foundation
|
||||
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||
import AppKit
|
||||
|
||||
public extension Pigment {
|
||||
/// Initialize a `Pigment` using an `NSColor`.
|
||||
init(_ color: NSColor) {
|
||||
red = color.redComponent
|
||||
green = color.greenComponent
|
||||
blue = color.blueComponent
|
||||
alpha = color.alphaComponent
|
||||
}
|
||||
|
||||
var nsColor: NSColor {
|
||||
NSColor(red: red, green: green, blue: blue, alpha: alpha)
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSColor {
|
||||
var pigment: Pigment {
|
||||
Pigment(self)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
39
third-party/SwiftColor/Sources/Pigment+CoreGraphics.swift
vendored
Normal file
39
third-party/SwiftColor/Sources/Pigment+CoreGraphics.swift
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import Foundation
|
||||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
|
||||
public extension Pigment {
|
||||
/// Initialize a `Pigment` using an `CGColor`.
|
||||
init?(_ color: CGColor) {
|
||||
let components = color.components ?? []
|
||||
switch components.count {
|
||||
case 2:
|
||||
// Monochrome/Grayscale
|
||||
// TODO: Express this in 'Color'.
|
||||
red = 0.0
|
||||
green = 0.0
|
||||
blue = 0.0
|
||||
alpha = components[1]
|
||||
case 4:
|
||||
// RGB
|
||||
red = components[0]
|
||||
green = components[1]
|
||||
blue = components[2]
|
||||
alpha = components[3]
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension CGColor {
|
||||
static func make(_ color: Pigment) -> CGColor {
|
||||
if color.red == 0.0, color.green == 0.0, color.blue == 0.0, color.alpha == 0.0 {
|
||||
// Return the CG color equivalent of `UIColor.clear`.
|
||||
return CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.0)
|
||||
}
|
||||
|
||||
return CGColor(srgbRed: color.red, green: color.green, blue: color.blue, alpha: color.alpha)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
95
third-party/SwiftColor/Sources/Pigment+Float.swift
vendored
Normal file
95
third-party/SwiftColor/Sources/Pigment+Float.swift
vendored
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import Foundation
|
||||
|
||||
public extension Pigment {
|
||||
/// Initialize a `Pigment` using `Float` values leaning towards the 'Red' spectrum.
|
||||
///
|
||||
/// - parameters
|
||||
/// - red: A value in the range of 0.0 to 1.0 representing the **red** percent.
|
||||
/// - green: A value in the range of 0.0 to 1.0 representing the **green** percent.
|
||||
/// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent.
|
||||
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
|
||||
init(
|
||||
red: Float,
|
||||
green: Float = 0.0,
|
||||
blue: Float = 0.0,
|
||||
alpha: Float = 1.0
|
||||
) {
|
||||
self.red = Double(red)
|
||||
self.green = Double(green)
|
||||
self.blue = Double(blue)
|
||||
self.alpha = Double(alpha)
|
||||
}
|
||||
|
||||
/// Initialize a `Pigment` using `Float` values leaning towards the 'Green' spectrum.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - green: A value in the range of 0.0 to 1.0 representing the **green** percent.
|
||||
/// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent.
|
||||
/// - red: A value in the range of 0.0 to 1.0 representing the **red** percent.
|
||||
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
|
||||
init(
|
||||
green: Float,
|
||||
blue: Float = 0.0,
|
||||
red: Float = 0.0,
|
||||
alpha: Float = 1.0
|
||||
) {
|
||||
self.red = Double(red)
|
||||
self.green = Double(green)
|
||||
self.blue = Double(blue)
|
||||
self.alpha = Double(alpha)
|
||||
}
|
||||
|
||||
/// Initialize a `Pigment` using `Float` values leaning towards the 'Blue' spectrum.
|
||||
///
|
||||
/// - parameter:
|
||||
/// - blue: A value in the range of 0.0 to 1.0 representing the **blue** percent.
|
||||
/// - red: A value in the range of 0.0 to 1.0 representing the **red** percent.
|
||||
/// - green: A value in the range of 0.0 to 1.0 representing the **green** percent.
|
||||
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
|
||||
init(
|
||||
blue: Float,
|
||||
red: Float = 0.0,
|
||||
green: Float = 0.0,
|
||||
alpha: Float = 1.0
|
||||
) {
|
||||
self.red = Double(red)
|
||||
self.green = Double(green)
|
||||
self.blue = Double(blue)
|
||||
self.alpha = Double(alpha)
|
||||
}
|
||||
|
||||
/// Initialize a `Pigment` using variadic `Float` values.
|
||||
///
|
||||
/// All _values_ should be expressed in the range of 0.0 to 1.0.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - values: A number of `Float` which are mapped to **red**, **green**, **blue** in that order.
|
||||
/// - alpha: Amount of _opacity/transparency_ to apply.
|
||||
init(
|
||||
_ values: Float...,
|
||||
alpha: Float
|
||||
) {
|
||||
if values.count > 0 {
|
||||
red = Double(values[0].clamped(to: 0 ... 1))
|
||||
} else {
|
||||
red = 0.0
|
||||
}
|
||||
if values.count > 1 {
|
||||
green = Double(values[1].clamped(to: 0 ... 1))
|
||||
} else {
|
||||
green = 0.0
|
||||
}
|
||||
if values.count > 2 {
|
||||
blue = Double(values[2].clamped(to: 0 ... 1))
|
||||
} else {
|
||||
blue = 0.0
|
||||
}
|
||||
self.alpha = Double(alpha.clamped(to: 0 ... 1))
|
||||
}
|
||||
}
|
||||
|
||||
extension Float {
|
||||
func clamped(to range: ClosedRange<Float>) -> Float {
|
||||
min(max(range.lowerBound, self), range.upperBound)
|
||||
}
|
||||
}
|
||||
145
third-party/SwiftColor/Sources/Pigment+Hex.swift
vendored
Normal file
145
third-party/SwiftColor/Sources/Pigment+Hex.swift
vendored
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import Foundation
|
||||
|
||||
public extension Pigment {
|
||||
/// Initializes a `Pigment` with an `Int` in the expected format of **0x000**.
|
||||
///
|
||||
/// Used as a short-hand. If the hex 0x123 is provided, it is interpreted as 0x112233.
|
||||
init(
|
||||
hex3 hex: Int,
|
||||
@Clamping(0 ... 1) alpha: Double = 1.0
|
||||
) {
|
||||
let values = Self.hex3(hex: hex, alpha: alpha)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
self.alpha = values.alpha
|
||||
}
|
||||
|
||||
/// Shorthand **0x0000** initializer similar to `init(hex3:alpha:)` where
|
||||
/// the last digit represent the alpha component.
|
||||
init(
|
||||
hex4 hex: Int
|
||||
) {
|
||||
let values = Self.hex4(hex: hex)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
alpha = values.alpha
|
||||
}
|
||||
|
||||
/// Initializes with a standard format hex representation of color in the form of **0x1E2C3D**.
|
||||
init(
|
||||
hex6 hex: Int,
|
||||
@Clamping(0 ... 1) alpha: Double = 1.0
|
||||
) {
|
||||
let values = Self.hex6(hex: hex, alpha: alpha)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
self.alpha = values.alpha
|
||||
}
|
||||
|
||||
#if !os(watchOS)
|
||||
/// Extended form of `init(hex6:alpha:)` expecting **0x112233FF**, that uses the last
|
||||
/// bits for the alpha component.
|
||||
init(
|
||||
hex8 hex: Int
|
||||
) {
|
||||
let values = Self.hex8(hex: hex)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
alpha = values.alpha
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Initializes a `Pigment` with an `Int` representation of an RGB(a) Hex Value
|
||||
///
|
||||
/// This initializer will do its best to interpret the intentions of what is provided.
|
||||
/// **YOUR RESULTS WILL VARY**, and it's best to use one of the `init(hex?:)` initializers.
|
||||
///
|
||||
/// - Parameter hex: Hex value
|
||||
/// - Parameter alpha: The opacity value of the color object
|
||||
init(
|
||||
_ hex: Int,
|
||||
alpha: Double? = nil
|
||||
) {
|
||||
#if !os(watchOS)
|
||||
if hex > 0xFFFFFF {
|
||||
let values = Self.hex8(hex: hex)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
self.alpha = values.alpha
|
||||
return
|
||||
}
|
||||
#endif
|
||||
if hex > 0xFFFF {
|
||||
let values = Self.hex6(hex: hex, alpha: alpha ?? 1.0)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
self.alpha = values.alpha
|
||||
} else if hex > 0xFFF {
|
||||
let values = Self.hex4(hex: hex)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
self.alpha = values.alpha
|
||||
} else {
|
||||
let values = Self.hex3(hex: hex, alpha: alpha ?? 1.0)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
self.alpha = values.alpha
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Pigment {
|
||||
static func hex3(
|
||||
hex: Int,
|
||||
@Clamping(0 ... 1) alpha: Double = 1.0
|
||||
) -> (red: Double, green: Double, blue: Double, alpha: Double) {
|
||||
let red = Double(duplicateBits((hex & 0xF00) >> 8)) / 255.0
|
||||
let green = Double(duplicateBits((hex & 0x0F0) >> 4)) / 255.0
|
||||
let blue = Double(duplicateBits((hex & 0x00F) >> 0)) / 255.0
|
||||
return (red, green, blue, alpha)
|
||||
}
|
||||
|
||||
static func hex4(
|
||||
hex: Int
|
||||
) -> (red: Double, green: Double, blue: Double, alpha: Double) {
|
||||
let red = Double(duplicateBits((hex & 0xF000) >> 12)) / 255.0
|
||||
let green = Double(duplicateBits((hex & 0x0F00) >> 8)) / 255.0
|
||||
let blue = Double(duplicateBits((hex & 0x00F0) >> 4)) / 255.0
|
||||
let alpha = Double(duplicateBits((hex & 0x000F) >> 0)) / 255.0
|
||||
return (red, green, blue, alpha)
|
||||
}
|
||||
|
||||
static func hex6(
|
||||
hex: Int,
|
||||
@Clamping(0 ... 1) alpha: Double = 1.0
|
||||
) -> (red: Double, green: Double, blue: Double, alpha: Double) {
|
||||
let red = Double((hex & 0xFF0000) >> 16) / 255.0
|
||||
let green = Double((hex & 0x00FF00) >> 8) / 255.0
|
||||
let blue = Double((hex & 0x0000FF) >> 0) / 255.0
|
||||
return (red, green, blue, alpha)
|
||||
}
|
||||
|
||||
#if !os(watchOS)
|
||||
static func hex8(
|
||||
hex: Int
|
||||
) -> (red: Double, green: Double, blue: Double, alpha: Double) {
|
||||
let red = Double((hex & 0xFF00_0000) >> 24) / 255.0
|
||||
let green = Double((hex & 0x00FF_0000) >> 16) / 255.0
|
||||
let blue = Double((hex & 0x0000_FF00) >> 8) / 255.0
|
||||
let alpha = Double((hex & 0x0000_00FF) >> 0) / 255.0
|
||||
return (red, green, blue, alpha)
|
||||
}
|
||||
#endif
|
||||
|
||||
static func duplicateBits(_ value: Int) -> Int {
|
||||
(value << 4) + value
|
||||
}
|
||||
}
|
||||
89
third-party/SwiftColor/Sources/Pigment+Int.swift
vendored
Normal file
89
third-party/SwiftColor/Sources/Pigment+Int.swift
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import Foundation
|
||||
|
||||
public extension Pigment {
|
||||
/// Initialize a `Pigment` using `Int` values leaning towards the 'Red' spectrum.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - red: A value in the range of 0 to 255 representing the **red** bits
|
||||
/// - green: A value in the range of 0 to 255 representing the **green** bits
|
||||
/// - blue: A value in the range of 0 to 255 representing the **blue** bits
|
||||
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
|
||||
init(
|
||||
@Clamping(0 ... 255) red: Int,
|
||||
@Clamping(0 ... 255) green: Int = 0,
|
||||
@Clamping(0 ... 255) blue: Int = 0,
|
||||
@Clamping(0.0 ... 1.0) alpha: Double = 1.0
|
||||
) {
|
||||
self.red = Double(red) / 255.0
|
||||
self.green = Double(green) / 255.0
|
||||
self.blue = Double(blue) / 255.0
|
||||
self.alpha = alpha
|
||||
}
|
||||
|
||||
/// Initialize a `Pigment` using `Int` values leaning towards the 'Green' spectrum.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - green: A value in the range of 0 to 255 representing the **green** bits
|
||||
/// - blue: A value in the range of 0 to 255 representing the **blue** bits
|
||||
/// - red: A value in the range of 0 to 255 representing the **red** bits
|
||||
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
|
||||
init(
|
||||
@Clamping(0 ... 255) green: Int,
|
||||
@Clamping(0 ... 255) blue: Int = 0,
|
||||
@Clamping(0 ... 255) red: Int = 0,
|
||||
@Clamping(0.0 ... 1.0) alpha: Double = 1.0
|
||||
) {
|
||||
self.red = Double(red) / 255.0
|
||||
self.green = Double(green) / 255.0
|
||||
self.blue = Double(blue) / 255.0
|
||||
self.alpha = alpha
|
||||
}
|
||||
|
||||
/// Initialize a `Pigment` using `Int` values leaning towards the 'Blue' spectrum.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - blue: A value in the range of 0 to 255 representing the **blue** bits
|
||||
/// - red: A value in the range of 0 to 255 representing the **red** bits
|
||||
/// - green: A value in the range of 0 to 255 representing the **green** bits
|
||||
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
|
||||
init(
|
||||
@Clamping(0 ... 255) blue: Int,
|
||||
@Clamping(0 ... 255) red: Int = 0,
|
||||
@Clamping(0 ... 255) green: Int = 0,
|
||||
@Clamping(0.0 ... 1.0) alpha: Double = 1.0
|
||||
) {
|
||||
self.red = Double(red) / 255.0
|
||||
self.green = Double(green) / 255.0
|
||||
self.blue = Double(blue) / 255.0
|
||||
self.alpha = alpha
|
||||
}
|
||||
|
||||
/// Initialize a `Pigment` using variadic `Int` values.
|
||||
///
|
||||
/// All _values_ should be expressed in the range of 0 to 255.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - values: A number of `Int` which are mapped to **red**, **green**, **blue** in that order.
|
||||
/// - alpha: A value in the range of 0.0 to 1.0 representing the **alpha/opacity/transparency** percent.
|
||||
init(
|
||||
_ values: Int...,
|
||||
@Clamping(0 ... 1) alpha: Double
|
||||
) {
|
||||
if values.count > 0 {
|
||||
red = Double(values[0]) / 255.0
|
||||
} else {
|
||||
red = 0.0
|
||||
}
|
||||
if values.count > 1 {
|
||||
green = Double(values[1]) / 255.0
|
||||
} else {
|
||||
green = 0.0
|
||||
}
|
||||
if values.count > 2 {
|
||||
blue = Double(values[2]) / 255.0
|
||||
} else {
|
||||
blue = 0.0
|
||||
}
|
||||
self.alpha = alpha
|
||||
}
|
||||
}
|
||||
470
third-party/SwiftColor/Sources/Pigment+Name.swift
vendored
Normal file
470
third-party/SwiftColor/Sources/Pigment+Name.swift
vendored
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
import Foundation
|
||||
|
||||
public extension Pigment {
|
||||
@available(*, deprecated, renamed: "Name")
|
||||
typealias Keyword = Name
|
||||
|
||||
enum Name: String, CaseIterable {
|
||||
case aliceBlue
|
||||
case antiqueWhite
|
||||
case aqua
|
||||
case aquamarine
|
||||
case azure
|
||||
case beige
|
||||
case bisque
|
||||
case black
|
||||
case blanchedAlmond
|
||||
case blue
|
||||
case blueViolet
|
||||
case brown
|
||||
case burlywood
|
||||
case cadetBlue
|
||||
case chartreuse
|
||||
case chocolate
|
||||
case coral
|
||||
case cornflowerBlue
|
||||
case cornsilk
|
||||
case crimson
|
||||
case cyan
|
||||
case darkBlue
|
||||
case darkCyan
|
||||
case darkGoldenrod
|
||||
case darkGray
|
||||
case darkGreen
|
||||
case darkGrey
|
||||
case darkKhaki
|
||||
case darkMagenta
|
||||
case darkOliveGreen
|
||||
case darkOrange
|
||||
case darkOrchid
|
||||
case darkRed
|
||||
case darkSalmon
|
||||
case darkSeagreen
|
||||
case darkSlateBlue
|
||||
case darkSlateGray
|
||||
case darkSlateGrey
|
||||
case darkTurquoise
|
||||
case darkViolet
|
||||
case deepPink
|
||||
case deepSkyblue
|
||||
case dimGray
|
||||
case dimGrey
|
||||
case dodgerBlue
|
||||
case firebrick
|
||||
case floralWhite
|
||||
case forestGreen
|
||||
case fuchsia
|
||||
case gainsboro
|
||||
case ghostWhite
|
||||
case gold
|
||||
case goldenrod
|
||||
case gray
|
||||
case green
|
||||
case greenYellow
|
||||
case grey
|
||||
case honeydew
|
||||
case hotPink
|
||||
case indianRed
|
||||
case indigo
|
||||
case ivory
|
||||
case khaki
|
||||
case lavender
|
||||
case lavenderBlush
|
||||
case lawnGreen
|
||||
case lemonChiffon
|
||||
case lightBlue
|
||||
case lightCoral
|
||||
case lightCyan
|
||||
case lightGoldenrodYellow
|
||||
case lightGray
|
||||
case lightGreen
|
||||
case lightGrey
|
||||
case lightPink
|
||||
case lightSalmon
|
||||
case lightSeagreen
|
||||
case lightSkyBlue
|
||||
case lightSlateGray
|
||||
case lightSlateGrey
|
||||
case lightSteelBlue
|
||||
case lightYellow
|
||||
case lime
|
||||
case limeGreen
|
||||
case linen
|
||||
case magenta
|
||||
case maroon
|
||||
case mediumAquamarine
|
||||
case mediumBlue
|
||||
case mediumOrchid
|
||||
case mediumPurple
|
||||
case mediumSeagreen
|
||||
case mediumSlateBlue
|
||||
case mediumSpringGreen
|
||||
case mediumTurquoise
|
||||
case mediumVioletRed
|
||||
case midnightBlue
|
||||
case mintCream
|
||||
case mistyRose
|
||||
case moccasin
|
||||
case navajoWhite
|
||||
case navy
|
||||
case oldLace
|
||||
case olive
|
||||
case oliveDrab
|
||||
case orange
|
||||
case orangeRed
|
||||
case orchid
|
||||
case paleGoldenrod
|
||||
case paleGreen
|
||||
case paleTurquoise
|
||||
case paleVioletRed
|
||||
case papayaWhip
|
||||
case peachPuff
|
||||
case peru
|
||||
case pink
|
||||
case plum
|
||||
case powderBlue
|
||||
case purple
|
||||
case red
|
||||
case rosyBrown
|
||||
case royalBlue
|
||||
case saddleBrown
|
||||
case salmon
|
||||
case sandyBrown
|
||||
case seagreen
|
||||
case seashell
|
||||
case sienna
|
||||
case silver
|
||||
case skyBlue
|
||||
case slateBlue
|
||||
case slateGray
|
||||
case slateGrey
|
||||
case snow
|
||||
case springGreen
|
||||
case steelBlue
|
||||
case tan
|
||||
case teal
|
||||
case thistle
|
||||
case tomato
|
||||
case turquoise
|
||||
case violet
|
||||
case wheat
|
||||
case white
|
||||
case whitesmoke
|
||||
case yellow
|
||||
case yellowGreen
|
||||
|
||||
public var rgb: (red: Int, green: Int, blue: Int) {
|
||||
switch self {
|
||||
case .aliceBlue: (240, 248, 255)
|
||||
case .antiqueWhite: (250, 235, 215)
|
||||
case .aqua: (0, 255, 255)
|
||||
case .aquamarine: (127, 255, 212)
|
||||
case .azure: (240, 255, 255)
|
||||
case .beige: (245, 245, 220)
|
||||
case .bisque: (255, 228, 196)
|
||||
case .black: (0, 0, 0)
|
||||
case .blanchedAlmond: (255, 235, 205)
|
||||
case .blue: (0, 0, 255)
|
||||
case .blueViolet: (138, 43, 226)
|
||||
case .brown: (165, 42, 42)
|
||||
case .burlywood: (222, 184, 135)
|
||||
case .cadetBlue: (95, 158, 160)
|
||||
case .chartreuse: (127, 255, 0)
|
||||
case .chocolate: (210, 105, 30)
|
||||
case .coral: (255, 127, 80)
|
||||
case .cornflowerBlue: (100, 149, 237)
|
||||
case .cornsilk: (255, 248, 220)
|
||||
case .crimson: (220, 20, 60)
|
||||
case .cyan: (0, 255, 255)
|
||||
case .darkBlue: (0, 0, 139)
|
||||
case .darkCyan: (0, 139, 139)
|
||||
case .darkGoldenrod: (184, 134, 11)
|
||||
case .darkGray: (169, 169, 169)
|
||||
case .darkGreen: (0, 100, 0)
|
||||
case .darkGrey: (169, 169, 169)
|
||||
case .darkKhaki: (189, 183, 107)
|
||||
case .darkMagenta: (139, 0, 139)
|
||||
case .darkOliveGreen: (85, 107, 47)
|
||||
case .darkOrange: (255, 140, 0)
|
||||
case .darkOrchid: (153, 50, 204)
|
||||
case .darkRed: (139, 0, 0)
|
||||
case .darkSalmon: (233, 150, 122)
|
||||
case .darkSeagreen: (143, 188, 143)
|
||||
case .darkSlateBlue: (72, 61, 139)
|
||||
case .darkSlateGray: (47, 79, 79)
|
||||
case .darkSlateGrey: (47, 79, 79)
|
||||
case .darkTurquoise: (0, 206, 209)
|
||||
case .darkViolet: (148, 0, 211)
|
||||
case .deepPink: (255, 20, 147)
|
||||
case .deepSkyblue: (0, 191, 255)
|
||||
case .dimGray: (105, 105, 105)
|
||||
case .dimGrey: (105, 105, 105)
|
||||
case .dodgerBlue: (30, 144, 255)
|
||||
case .firebrick: (178, 34, 34)
|
||||
case .floralWhite: (255, 250, 240)
|
||||
case .forestGreen: (34, 139, 34)
|
||||
case .fuchsia: (255, 0, 255)
|
||||
case .gainsboro: (220, 220, 220)
|
||||
case .ghostWhite: (248, 248, 255)
|
||||
case .gold: (255, 215, 0)
|
||||
case .goldenrod: (218, 165, 32)
|
||||
case .gray: (128, 128, 128)
|
||||
case .green: (0, 128, 0)
|
||||
case .greenYellow: (173, 255, 47)
|
||||
case .grey: (128, 128, 128)
|
||||
case .honeydew: (240, 255, 240)
|
||||
case .hotPink: (255, 105, 180)
|
||||
case .indianRed: (205, 92, 92)
|
||||
case .indigo: (75, 0, 130)
|
||||
case .ivory: (255, 255, 240)
|
||||
case .khaki: (240, 230, 140)
|
||||
case .lavender: (230, 230, 250)
|
||||
case .lavenderBlush: (255, 240, 245)
|
||||
case .lawnGreen: (124, 252, 0)
|
||||
case .lemonChiffon: (255, 250, 205)
|
||||
case .lightBlue: (173, 216, 230)
|
||||
case .lightCoral: (240, 128, 128)
|
||||
case .lightCyan: (224, 255, 255)
|
||||
case .lightGoldenrodYellow: (250, 250, 210)
|
||||
case .lightGray: (211, 211, 211)
|
||||
case .lightGreen: (144, 238, 144)
|
||||
case .lightGrey: (211, 211, 211)
|
||||
case .lightPink: (255, 182, 193)
|
||||
case .lightSalmon: (255, 160, 122)
|
||||
case .lightSeagreen: (32, 178, 170)
|
||||
case .lightSkyBlue: (135, 206, 250)
|
||||
case .lightSlateGray: (119, 136, 153)
|
||||
case .lightSlateGrey: (119, 136, 153)
|
||||
case .lightSteelBlue: (176, 196, 222)
|
||||
case .lightYellow: (255, 255, 224)
|
||||
case .lime: (0, 255, 0)
|
||||
case .limeGreen: (50, 205, 50)
|
||||
case .linen: (250, 240, 230)
|
||||
case .magenta: (255, 0, 255)
|
||||
case .maroon: (128, 0, 0)
|
||||
case .mediumAquamarine: (102, 205, 170)
|
||||
case .mediumBlue: (0, 0, 205)
|
||||
case .mediumOrchid: (186, 85, 211)
|
||||
case .mediumPurple: (147, 112, 219)
|
||||
case .mediumSeagreen: (60, 179, 113)
|
||||
case .mediumSlateBlue: (123, 104, 238)
|
||||
case .mediumSpringGreen: (0, 250, 154)
|
||||
case .mediumTurquoise: (72, 209, 204)
|
||||
case .mediumVioletRed: (199, 21, 133)
|
||||
case .midnightBlue: (25, 25, 112)
|
||||
case .mintCream: (245, 255, 250)
|
||||
case .mistyRose: (255, 228, 225)
|
||||
case .moccasin: (255, 228, 181)
|
||||
case .navajoWhite: (255, 222, 173)
|
||||
case .navy: (0, 0, 128)
|
||||
case .oldLace: (253, 245, 230)
|
||||
case .olive: (128, 128, 0)
|
||||
case .oliveDrab: (107, 142, 35)
|
||||
case .orange: (255, 165, 0)
|
||||
case .orangeRed: (255, 69, 0)
|
||||
case .orchid: (218, 112, 214)
|
||||
case .paleGoldenrod: (238, 232, 170)
|
||||
case .paleGreen: (152, 251, 152)
|
||||
case .paleTurquoise: (175, 238, 238)
|
||||
case .paleVioletRed: (219, 112, 147)
|
||||
case .papayaWhip: (255, 239, 213)
|
||||
case .peachPuff: (255, 218, 185)
|
||||
case .peru: (205, 133, 63)
|
||||
case .pink: (255, 192, 203)
|
||||
case .plum: (221, 160, 221)
|
||||
case .powderBlue: (176, 224, 230)
|
||||
case .purple: (128, 0, 128)
|
||||
case .red: (255, 0, 0)
|
||||
case .rosyBrown: (188, 143, 143)
|
||||
case .royalBlue: (65, 105, 225)
|
||||
case .saddleBrown: (139, 69, 19)
|
||||
case .salmon: (250, 128, 114)
|
||||
case .sandyBrown: (244, 164, 96)
|
||||
case .seagreen: (46, 139, 87)
|
||||
case .seashell: (255, 245, 238)
|
||||
case .sienna: (160, 82, 45)
|
||||
case .silver: (192, 192, 192)
|
||||
case .skyBlue: (135, 206, 235)
|
||||
case .slateBlue: (106, 90, 205)
|
||||
case .slateGray: (112, 128, 144)
|
||||
case .slateGrey: (112, 128, 144)
|
||||
case .snow: (255, 250, 250)
|
||||
case .springGreen: (0, 255, 127)
|
||||
case .steelBlue: (70, 130, 180)
|
||||
case .tan: (210, 180, 140)
|
||||
case .teal: (0, 128, 128)
|
||||
case .thistle: (216, 191, 216)
|
||||
case .tomato: (255, 99, 71)
|
||||
case .turquoise: (64, 224, 208)
|
||||
case .violet: (238, 130, 238)
|
||||
case .wheat: (245, 222, 179)
|
||||
case .white: (255, 255, 255)
|
||||
case .whitesmoke: (245, 245, 245)
|
||||
case .yellow: (255, 255, 0)
|
||||
case .yellowGreen: (154, 205, 50)
|
||||
}
|
||||
}
|
||||
|
||||
public var pigment: Pigment {
|
||||
switch self {
|
||||
case .aliceBlue: Pigment(240, 248, 255, alpha: 1.0)
|
||||
case .antiqueWhite: Pigment(250, 235, 215, alpha: 1.0)
|
||||
case .aqua: Pigment(0, 255, 255, alpha: 1.0)
|
||||
case .aquamarine: Pigment(127, 255, 212, alpha: 1.0)
|
||||
case .azure: Pigment(240, 255, 255, alpha: 1.0)
|
||||
case .beige: Pigment(245, 245, 220, alpha: 1.0)
|
||||
case .bisque: Pigment(255, 228, 196, alpha: 1.0)
|
||||
case .black: Pigment(0, 0, 0, alpha: 1.0)
|
||||
case .blanchedAlmond: Pigment(255, 235, 205, alpha: 1.0)
|
||||
case .blue: Pigment(0, 0, 255, alpha: 1.0)
|
||||
case .blueViolet: Pigment(138, 43, 226, alpha: 1.0)
|
||||
case .brown: Pigment(165, 42, 42, alpha: 1.0)
|
||||
case .burlywood: Pigment(222, 184, 135, alpha: 1.0)
|
||||
case .cadetBlue: Pigment(95, 158, 160, alpha: 1.0)
|
||||
case .chartreuse: Pigment(127, 255, 0, alpha: 1.0)
|
||||
case .chocolate: Pigment(210, 105, 30, alpha: 1.0)
|
||||
case .coral: Pigment(255, 127, 80, alpha: 1.0)
|
||||
case .cornflowerBlue: Pigment(100, 149, 237, alpha: 1.0)
|
||||
case .cornsilk: Pigment(255, 248, 220, alpha: 1.0)
|
||||
case .crimson: Pigment(220, 20, 60, alpha: 1.0)
|
||||
case .cyan: Pigment(0, 255, 255, alpha: 1.0)
|
||||
case .darkBlue: Pigment(0, 0, 139, alpha: 1.0)
|
||||
case .darkCyan: Pigment(0, 139, 139, alpha: 1.0)
|
||||
case .darkGoldenrod: Pigment(184, 134, 11, alpha: 1.0)
|
||||
case .darkGray: Pigment(169, 169, 169, alpha: 1.0)
|
||||
case .darkGreen: Pigment(0, 100, 0, alpha: 1.0)
|
||||
case .darkGrey: Pigment(169, 169, 169, alpha: 1.0)
|
||||
case .darkKhaki: Pigment(189, 183, 107, alpha: 1.0)
|
||||
case .darkMagenta: Pigment(139, 0, 139, alpha: 1.0)
|
||||
case .darkOliveGreen: Pigment(85, 107, 47, alpha: 1.0)
|
||||
case .darkOrange: Pigment(255, 140, 0, alpha: 1.0)
|
||||
case .darkOrchid: Pigment(153, 50, 204, alpha: 1.0)
|
||||
case .darkRed: Pigment(139, 0, 0, alpha: 1.0)
|
||||
case .darkSalmon: Pigment(233, 150, 122, alpha: 1.0)
|
||||
case .darkSeagreen: Pigment(143, 188, 143, alpha: 1.0)
|
||||
case .darkSlateBlue: Pigment(72, 61, 139, alpha: 1.0)
|
||||
case .darkSlateGray: Pigment(47, 79, 79, alpha: 1.0)
|
||||
case .darkSlateGrey: Pigment(47, 79, 79, alpha: 1.0)
|
||||
case .darkTurquoise: Pigment(0, 206, 209, alpha: 1.0)
|
||||
case .darkViolet: Pigment(148, 0, 211, alpha: 1.0)
|
||||
case .deepPink: Pigment(255, 20, 147, alpha: 1.0)
|
||||
case .deepSkyblue: Pigment(0, 191, 255, alpha: 1.0)
|
||||
case .dimGray: Pigment(105, 105, 105, alpha: 1.0)
|
||||
case .dimGrey: Pigment(105, 105, 105, alpha: 1.0)
|
||||
case .dodgerBlue: Pigment(30, 144, 255, alpha: 1.0)
|
||||
case .firebrick: Pigment(178, 34, 34, alpha: 1.0)
|
||||
case .floralWhite: Pigment(255, 250, 240, alpha: 1.0)
|
||||
case .forestGreen: Pigment(34, 139, 34, alpha: 1.0)
|
||||
case .fuchsia: Pigment(255, 0, 255, alpha: 1.0)
|
||||
case .gainsboro: Pigment(220, 220, 220, alpha: 1.0)
|
||||
case .ghostWhite: Pigment(248, 248, 255, alpha: 1.0)
|
||||
case .gold: Pigment(255, 215, 0, alpha: 1.0)
|
||||
case .goldenrod: Pigment(218, 165, 32, alpha: 1.0)
|
||||
case .gray: Pigment(128, 128, 128, alpha: 1.0)
|
||||
case .green: Pigment(0, 128, 0, alpha: 1.0)
|
||||
case .greenYellow: Pigment(173, 255, 47, alpha: 1.0)
|
||||
case .grey: Pigment(128, 128, 128, alpha: 1.0)
|
||||
case .honeydew: Pigment(240, 255, 240, alpha: 1.0)
|
||||
case .hotPink: Pigment(255, 105, 180, alpha: 1.0)
|
||||
case .indianRed: Pigment(205, 92, 92, alpha: 1.0)
|
||||
case .indigo: Pigment(75, 0, 130, alpha: 1.0)
|
||||
case .ivory: Pigment(255, 255, 240, alpha: 1.0)
|
||||
case .khaki: Pigment(240, 230, 140, alpha: 1.0)
|
||||
case .lavender: Pigment(230, 230, 250, alpha: 1.0)
|
||||
case .lavenderBlush: Pigment(255, 240, 245, alpha: 1.0)
|
||||
case .lawnGreen: Pigment(124, 252, 0, alpha: 1.0)
|
||||
case .lemonChiffon: Pigment(255, 250, 205, alpha: 1.0)
|
||||
case .lightBlue: Pigment(173, 216, 230, alpha: 1.0)
|
||||
case .lightCoral: Pigment(240, 128, 128, alpha: 1.0)
|
||||
case .lightCyan: Pigment(224, 255, 255, alpha: 1.0)
|
||||
case .lightGoldenrodYellow: Pigment(250, 250, 210, alpha: 1.0)
|
||||
case .lightGray: Pigment(211, 211, 211, alpha: 1.0)
|
||||
case .lightGreen: Pigment(144, 238, 144, alpha: 1.0)
|
||||
case .lightGrey: Pigment(211, 211, 211, alpha: 1.0)
|
||||
case .lightPink: Pigment(255, 182, 193, alpha: 1.0)
|
||||
case .lightSalmon: Pigment(255, 160, 122, alpha: 1.0)
|
||||
case .lightSeagreen: Pigment(32, 178, 170, alpha: 1.0)
|
||||
case .lightSkyBlue: Pigment(135, 206, 250, alpha: 1.0)
|
||||
case .lightSlateGray: Pigment(119, 136, 153, alpha: 1.0)
|
||||
case .lightSlateGrey: Pigment(119, 136, 153, alpha: 1.0)
|
||||
case .lightSteelBlue: Pigment(176, 196, 222, alpha: 1.0)
|
||||
case .lightYellow: Pigment(255, 255, 224, alpha: 1.0)
|
||||
case .lime: Pigment(0, 255, 0, alpha: 1.0)
|
||||
case .limeGreen: Pigment(50, 205, 50, alpha: 1.0)
|
||||
case .linen: Pigment(250, 240, 230, alpha: 1.0)
|
||||
case .magenta: Pigment(255, 0, 255, alpha: 1.0)
|
||||
case .maroon: Pigment(128, 0, 0, alpha: 1.0)
|
||||
case .mediumAquamarine: Pigment(102, 205, 170, alpha: 1.0)
|
||||
case .mediumBlue: Pigment(0, 0, 205, alpha: 1.0)
|
||||
case .mediumOrchid: Pigment(186, 85, 211, alpha: 1.0)
|
||||
case .mediumPurple: Pigment(147, 112, 219, alpha: 1.0)
|
||||
case .mediumSeagreen: Pigment(60, 179, 113, alpha: 1.0)
|
||||
case .mediumSlateBlue: Pigment(123, 104, 238, alpha: 1.0)
|
||||
case .mediumSpringGreen: Pigment(0, 250, 154, alpha: 1.0)
|
||||
case .mediumTurquoise: Pigment(72, 209, 204, alpha: 1.0)
|
||||
case .mediumVioletRed: Pigment(199, 21, 133, alpha: 1.0)
|
||||
case .midnightBlue: Pigment(25, 25, 112, alpha: 1.0)
|
||||
case .mintCream: Pigment(245, 255, 250, alpha: 1.0)
|
||||
case .mistyRose: Pigment(255, 228, 225, alpha: 1.0)
|
||||
case .moccasin: Pigment(255, 228, 181, alpha: 1.0)
|
||||
case .navajoWhite: Pigment(255, 222, 173, alpha: 1.0)
|
||||
case .navy: Pigment(0, 0, 128, alpha: 1.0)
|
||||
case .oldLace: Pigment(253, 245, 230, alpha: 1.0)
|
||||
case .olive: Pigment(128, 128, 0, alpha: 1.0)
|
||||
case .oliveDrab: Pigment(107, 142, 35, alpha: 1.0)
|
||||
case .orange: Pigment(255, 165, 0, alpha: 1.0)
|
||||
case .orangeRed: Pigment(255, 69, 0, alpha: 1.0)
|
||||
case .orchid: Pigment(218, 112, 214, alpha: 1.0)
|
||||
case .paleGoldenrod: Pigment(238, 232, 170, alpha: 1.0)
|
||||
case .paleGreen: Pigment(152, 251, 152, alpha: 1.0)
|
||||
case .paleTurquoise: Pigment(175, 238, 238, alpha: 1.0)
|
||||
case .paleVioletRed: Pigment(219, 112, 147, alpha: 1.0)
|
||||
case .papayaWhip: Pigment(255, 239, 213, alpha: 1.0)
|
||||
case .peachPuff: Pigment(255, 218, 185, alpha: 1.0)
|
||||
case .peru: Pigment(205, 133, 63, alpha: 1.0)
|
||||
case .pink: Pigment(255, 192, 203, alpha: 1.0)
|
||||
case .plum: Pigment(221, 160, 221, alpha: 1.0)
|
||||
case .powderBlue: Pigment(176, 224, 230, alpha: 1.0)
|
||||
case .purple: Pigment(128, 0, 128, alpha: 1.0)
|
||||
case .red: Pigment(255, 0, 0, alpha: 1.0)
|
||||
case .rosyBrown: Pigment(188, 143, 143, alpha: 1.0)
|
||||
case .royalBlue: Pigment(65, 105, 225, alpha: 1.0)
|
||||
case .saddleBrown: Pigment(139, 69, 19, alpha: 1.0)
|
||||
case .salmon: Pigment(250, 128, 114, alpha: 1.0)
|
||||
case .sandyBrown: Pigment(244, 164, 96, alpha: 1.0)
|
||||
case .seagreen: Pigment(46, 139, 87, alpha: 1.0)
|
||||
case .seashell: Pigment(255, 245, 238, alpha: 1.0)
|
||||
case .sienna: Pigment(160, 82, 45, alpha: 1.0)
|
||||
case .silver: Pigment(192, 192, 192, alpha: 1.0)
|
||||
case .skyBlue: Pigment(135, 206, 235, alpha: 1.0)
|
||||
case .slateBlue: Pigment(106, 90, 205, alpha: 1.0)
|
||||
case .slateGray: Pigment(112, 128, 144, alpha: 1.0)
|
||||
case .slateGrey: Pigment(112, 128, 144, alpha: 1.0)
|
||||
case .snow: Pigment(255, 250, 250, alpha: 1.0)
|
||||
case .springGreen: Pigment(0, 255, 127, alpha: 1.0)
|
||||
case .steelBlue: Pigment(70, 130, 180, alpha: 1.0)
|
||||
case .tan: Pigment(210, 180, 140, alpha: 1.0)
|
||||
case .teal: Pigment(0, 128, 128, alpha: 1.0)
|
||||
case .thistle: Pigment(216, 191, 216, alpha: 1.0)
|
||||
case .tomato: Pigment(255, 99, 71, alpha: 1.0)
|
||||
case .turquoise: Pigment(64, 224, 208, alpha: 1.0)
|
||||
case .violet: Pigment(238, 130, 238, alpha: 1.0)
|
||||
case .wheat: Pigment(245, 222, 179, alpha: 1.0)
|
||||
case .white: Pigment(255, 255, 255, alpha: 1.0)
|
||||
case .whitesmoke: Pigment(245, 245, 245, alpha: 1.0)
|
||||
case .yellow: Pigment(255, 255, 0, alpha: 1.0)
|
||||
case .yellowGreen: Pigment(154, 205, 50, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
_ name: Name,
|
||||
@Clamping(0 ... 1) alpha: Double = 1.0
|
||||
) {
|
||||
red = Double(name.rgb.red) / 255.0
|
||||
green = Double(name.rgb.green) / 255.0
|
||||
blue = Double(name.rgb.blue) / 255.0
|
||||
self.alpha = alpha
|
||||
}
|
||||
}
|
||||
76
third-party/SwiftColor/Sources/Pigment+String.swift
vendored
Normal file
76
third-party/SwiftColor/Sources/Pigment+String.swift
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import Foundation
|
||||
|
||||
public extension Pigment {
|
||||
init(_ value: String, alpha: Double = 1.0) {
|
||||
if let keyword = Name.allCases.first(where: { $0.rawValue.caseInsensitiveCompare(value) == .orderedSame }) {
|
||||
red = keyword.pigment.red
|
||||
green = keyword.pigment.green
|
||||
blue = keyword.pigment.blue
|
||||
self.alpha = alpha
|
||||
return
|
||||
}
|
||||
|
||||
if ExtendedKeyword.allCases.contains(where: { $0.rawValue.caseInsensitiveCompare(value) == .orderedSame }) {
|
||||
red = 1.0
|
||||
green = 1.0
|
||||
blue = 1.0
|
||||
self.alpha = alpha
|
||||
return
|
||||
}
|
||||
|
||||
var hex = value
|
||||
if hex.hasPrefix("#") {
|
||||
hex = String(hex.dropFirst())
|
||||
}
|
||||
|
||||
guard let hexValue = Int(hex, radix: 16) else {
|
||||
red = 1.0
|
||||
green = 1.0
|
||||
blue = 1.0
|
||||
self.alpha = alpha
|
||||
return
|
||||
}
|
||||
|
||||
switch hex.count {
|
||||
case 3:
|
||||
let values = Self.hex3(hex: hexValue)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
self.alpha = values.alpha
|
||||
case 4:
|
||||
let values = Self.hex4(hex: hexValue)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
self.alpha = values.alpha
|
||||
case 6:
|
||||
let values = Self.hex6(hex: hexValue, alpha: alpha)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
self.alpha = values.alpha
|
||||
#if !os(watchOS)
|
||||
case 8:
|
||||
let values = Self.hex8(hex: hexValue)
|
||||
red = values.red
|
||||
green = values.green
|
||||
blue = values.blue
|
||||
self.alpha = values.alpha
|
||||
#endif
|
||||
default:
|
||||
red = 1.0
|
||||
green = 1.0
|
||||
blue = 1.0
|
||||
self.alpha = alpha
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Pigment {
|
||||
enum ExtendedKeyword: String, CaseIterable {
|
||||
case none
|
||||
case clear
|
||||
case transparent
|
||||
}
|
||||
}
|
||||
9
third-party/SwiftColor/Sources/Pigment+SwiftUI.swift
vendored
Normal file
9
third-party/SwiftColor/Sources/Pigment+SwiftUI.swift
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#if canImport(SwiftUI)
|
||||
import SwiftUI
|
||||
|
||||
public extension Pigment {
|
||||
var color: Color {
|
||||
Color(red: red, green: green, blue: blue, opacity: alpha)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
37
third-party/SwiftColor/Sources/Pigment+UIKit.swift
vendored
Normal file
37
third-party/SwiftColor/Sources/Pigment+UIKit.swift
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import Foundation
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
|
||||
public extension Pigment {
|
||||
init(_ color: UIColor) {
|
||||
var redComponent: CGFloat = 1.0
|
||||
var greenComponent: CGFloat = 1.0
|
||||
var blueComponent: CGFloat = 1.0
|
||||
var alphaComponent: CGFloat = 1.0
|
||||
|
||||
guard color.getRed(&redComponent, green: &greenComponent, blue: &blueComponent, alpha: &alphaComponent) else {
|
||||
// TODO: Fail Initializer? Default Colors?
|
||||
red = redComponent
|
||||
green = greenComponent
|
||||
blue = blueComponent
|
||||
alpha = alphaComponent
|
||||
return
|
||||
}
|
||||
|
||||
red = redComponent
|
||||
green = greenComponent
|
||||
blue = blueComponent
|
||||
alpha = alphaComponent
|
||||
}
|
||||
|
||||
var uiColor: UIColor {
|
||||
UIColor(red: red, green: green, blue: blue, alpha: alpha)
|
||||
}
|
||||
}
|
||||
|
||||
public extension UIColor {
|
||||
var pigment: Pigment {
|
||||
Pigment(self)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
53
third-party/SwiftColor/Sources/Pigment.swift
vendored
Normal file
53
third-party/SwiftColor/Sources/Pigment.swift
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/// A platform agnostic representation of Color
|
||||
///
|
||||
/// The components - red, green, blue, & alpha - are maintained as a floating-point representation.
|
||||
/// Each value can range from 0.0 to 1.0 (e.g. 0 to 100 percent).
|
||||
///
|
||||
/// 'Pure White' is represented by values all equal to **1.0**.
|
||||
public struct Pigment: Sendable {
|
||||
|
||||
public let colorSpace: ColorSpace = .rgba
|
||||
public let red: Double
|
||||
public let green: Double
|
||||
public let blue: Double
|
||||
public let alpha: Double
|
||||
|
||||
public init(
|
||||
@Clamping(0 ... 1) red: Double = 1.0,
|
||||
@Clamping(0 ... 1) green: Double = 1.0,
|
||||
@Clamping(0 ... 1) blue: Double = 1.0,
|
||||
@Clamping(0 ... 1) alpha: Double = 1.0
|
||||
) {
|
||||
self.red = red
|
||||
self.green = green
|
||||
self.blue = blue
|
||||
self.alpha = alpha
|
||||
}
|
||||
}
|
||||
|
||||
extension Pigment: CustomStringConvertible {
|
||||
public var description: String {
|
||||
String(format: "Pigment(red: %.4f, green: %.4f, blue: %.4f, alpha: %.2f)", red, green, blue, alpha)
|
||||
}
|
||||
}
|
||||
|
||||
extension Pigment: Equatable {
|
||||
public static func == (lhs: Pigment, rhs: Pigment) -> Bool {
|
||||
guard lhs.red == rhs.red else {
|
||||
return false
|
||||
}
|
||||
guard lhs.green == rhs.green else {
|
||||
return false
|
||||
}
|
||||
guard lhs.blue == rhs.blue else {
|
||||
return false
|
||||
}
|
||||
guard lhs.alpha == rhs.alpha else {
|
||||
return false
|
||||
}
|
||||
guard lhs.colorSpace == rhs.colorSpace else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
18
third-party/SwiftMath/BUILD
vendored
Normal file
18
third-party/SwiftMath/BUILD
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SwiftMath",
|
||||
module_name = "SwiftMath",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AppBundle",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
21
third-party/SwiftMath/LICENSE
vendored
Executable file
21
third-party/SwiftMath/LICENSE
vendored
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Computer Inspirations
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
714
third-party/SwiftMath/README.md
vendored
Normal file
714
third-party/SwiftMath/README.md
vendored
Normal file
|
|
@ -0,0 +1,714 @@
|
|||
# SwiftMath
|
||||
|
||||
`SwiftMath` provides a full Swift implementation of [iosMath](https://travis-ci.org/kostub/iosMath)
|
||||
for displaying beautifully rendered math equations in iOS and MacOS applications. It typesets formulae written
|
||||
using LaTeX in a `UILabel` equivalent class. It uses the same typesetting rules as LaTeX and
|
||||
so the equations are rendered exactly as LaTeX would render them.
|
||||
|
||||
Please also check out [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo.git) for examples of how to use `SwiftMath`
|
||||
from SwiftUI.
|
||||
|
||||
`SwiftMath` is similar to [MathJax](https://www.mathjax.org) or
|
||||
[KaTeX](https://github.com/Khan/KaTeX) for the web but for native iOS or MacOS
|
||||
applications without having to use a `UIWebView` and Javascript. More
|
||||
importantly, it is significantly faster than using a `UIWebView`.
|
||||
|
||||
`SwiftMath` is a Swift translation of the latest `iosMath` v0.9.5 release but includes bug fixes
|
||||
and enhancements like a new \lbar (lambda bar) character and cyrillic alphabet support.
|
||||
The original `iosMath` test suites have also been translated to Swift and run without errors.
|
||||
Note: Error test conditions are ignored to avoid tagging everything with silly `throw`s.
|
||||
Please let me know of any bugs or bug fixes that you find.
|
||||
|
||||
`SwiftMath` prepackages everything needed for direct access via the Swift Package Manager.
|
||||
|
||||
## Examples
|
||||
Here are screenshots of some formulae that were rendered with this library:
|
||||
|
||||
```LaTeX
|
||||
x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```LaTeX
|
||||
f(x) = \int\limits_{-\infty}^\infty\!\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\xi
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```LaTeX
|
||||
\frac{1}{n}\sum_{i=1}^{n}x_i \geq \sqrt[n]{\prod_{i=1}^{n}x_i}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```LaTex
|
||||
\frac{1}{\left(\sqrt{\phi \sqrt{5}}-\phi\\right) e^{\frac25 \pi}}
|
||||
= 1+\frac{e^{-2\pi}} {1 +\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
More examples are included in [EXAMPLES](EXAMPLES.md)
|
||||
|
||||
## Fonts
|
||||
Here are previews of the included fonts:
|
||||
|
||||

|
||||

|
||||
|
||||
## Requirements
|
||||
`SwiftMath` works on iOS 11+ or MacOS 12+. It depends
|
||||
on the following Apple frameworks:
|
||||
|
||||
* Foundation.framework
|
||||
* CoreGraphics.framework
|
||||
* QuartzCore.framework
|
||||
* CoreText.framework
|
||||
|
||||
Additionally for iOS it requires:
|
||||
* UIKit.framework
|
||||
|
||||
Additionally for MacOS it requires:
|
||||
* AppKit.framework
|
||||
|
||||
## Installation
|
||||
|
||||
### Swift Package
|
||||
|
||||
`SwiftMath` is available from [SwiftMath](https://github.com/mgriebling/SwiftMath.git).
|
||||
To use it in your code, just add the https://github.com/mgriebling/SwiftMath.git path to
|
||||
XCode's package manager.
|
||||
|
||||
## Usage
|
||||
|
||||
The library provides a class `MTMathUILabel` which is a `UIView` that
|
||||
supports rendering math equations. To display an equation simply create
|
||||
an `MTMathUILabel` as follows:
|
||||
|
||||
```swift
|
||||
|
||||
import SwiftMath
|
||||
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}"
|
||||
|
||||
```
|
||||
Adding `MTMathUILabel` as a sub-view of your `UIView` will render the
|
||||
quadratic formula example shown above.
|
||||
|
||||
The following code creates a SwiftUI component called `MathView` encapsulating the MTMathUILabel:
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
import SwiftMath
|
||||
|
||||
struct MathView: UIViewRepresentable {
|
||||
var equation: String
|
||||
var font: MathFont = .latinModernFont
|
||||
var textAlignment: MTTextAlignment = .center
|
||||
var fontSize: CGFloat = 30
|
||||
var labelMode: MTMathUILabelMode = .text
|
||||
var insets: MTEdgeInsets = MTEdgeInsets()
|
||||
|
||||
func makeUIView(context: Context) -> MTMathUILabel {
|
||||
let view = MTMathUILabel()
|
||||
view.setContentHuggingPriority(.required, for: .vertical)
|
||||
view.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ view: MTMathUILabel, context: Context) {
|
||||
view.latex = equation
|
||||
let font = MTFontManager().font(withName: font.rawValue, size: fontSize)
|
||||
font?.fallbackFont = UIFont.systemFont(ofSize: fontSize)
|
||||
view.font = font
|
||||
view.textAlignment = textAlignment
|
||||
view.labelMode = labelMode
|
||||
view.textColor = MTColor(Color.primary)
|
||||
view.contentInsets = insets
|
||||
view.invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
func sizeThatFits(_ proposal: ProposedViewSize, uiView: MTMathUILabel, context: Context) -> CGSize? {
|
||||
// Enable line wrapping by passing proposed width to the label
|
||||
if let width = proposal.width, width.isFinite, width > 0 {
|
||||
uiView.preferredMaxLayoutWidth = width
|
||||
let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
|
||||
return size
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For code that works with SwiftUI running natively under MacOS use the following:
|
||||
|
||||
```swift
|
||||
import SwiftUI
|
||||
import SwiftMath
|
||||
|
||||
struct MathView: NSViewRepresentable {
|
||||
var equation: String
|
||||
var font: MathFont = .latinModernFont
|
||||
var textAlignment: MTTextAlignment = .center
|
||||
var fontSize: CGFloat = 30
|
||||
var labelMode: MTMathUILabelMode = .text
|
||||
var insets: MTEdgeInsets = MTEdgeInsets()
|
||||
|
||||
func makeNSView(context: Context) -> MTMathUILabel {
|
||||
let view = MTMathUILabel()
|
||||
view.setContentHuggingPriority(.required, for: .vertical)
|
||||
view.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ view: MTMathUILabel, context: Context) {
|
||||
view.latex = equation
|
||||
let font = MTFontManager().font(withName: font.rawValue, size: fontSize)
|
||||
font?.fallbackFont = NSFont.systemFont(ofSize: fontSize)
|
||||
view.font = font
|
||||
view.textAlignment = textAlignment
|
||||
view.labelMode = labelMode
|
||||
view.textColor = MTColor(Color.primary)
|
||||
view.contentInsets = insets
|
||||
view.invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
func sizeThatFits(_ proposal: ProposedViewSize, nsView: MTMathUILabel, context: Context) -> CGSize? {
|
||||
// Enable line wrapping by passing proposed width to the label
|
||||
if let width = proposal.width, width.isFinite, width > 0 {
|
||||
nsView.preferredMaxLayoutWidth = width
|
||||
let size = nsView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
|
||||
return size
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic Line Wrapping
|
||||
|
||||
`SwiftMath` supports automatic line wrapping (multiline display) for mathematical content. The implementation uses **interatom line breaking** which breaks equations at atom boundaries (between mathematical elements) rather than within them, preserving the semantic structure of the mathematics.
|
||||
|
||||
#### Using Line Wrapping with UIKit/AppKit
|
||||
|
||||
For direct `MTMathUILabel` usage, set the `preferredMaxLayoutWidth` property:
|
||||
|
||||
```swift
|
||||
let label = MTMathUILabel()
|
||||
label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5"
|
||||
label.font = MTFontManager.fontManager.defaultFont
|
||||
|
||||
// Enable line wrapping by setting a maximum width
|
||||
label.preferredMaxLayoutWidth = 235
|
||||
```
|
||||
|
||||
You can also use `sizeThatFits` to calculate the size with a width constraint:
|
||||
|
||||
```swift
|
||||
let constrainedSize = label.sizeThatFits(CGSize(width: 235, height: .greatestFiniteMagnitude))
|
||||
```
|
||||
|
||||
#### Using Line Wrapping with SwiftUI
|
||||
|
||||
The `MathView` examples above include `sizeThatFits()` which automatically enables line wrapping when SwiftUI proposes a width constraint. No additional configuration is needed:
|
||||
|
||||
```swift
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
MathView(
|
||||
equation: "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5",
|
||||
fontSize: 17,
|
||||
labelMode: .text
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: 235) // The equation will break across multiple lines
|
||||
```
|
||||
|
||||
#### Line Wrapping Behavior and Capabilities
|
||||
|
||||
SwiftMath implements **two complementary line breaking mechanisms**:
|
||||
|
||||
##### 1. Interatom Line Breaking (Primary)
|
||||
Breaks equations **between atoms** (mathematical elements) when content exceeds the width constraint. This is the preferred method as it maintains semantic integrity.
|
||||
|
||||
##### 2. Universal Line Breaking (Fallback)
|
||||
For very long text within single atoms, breaks at Unicode word boundaries using Core Text with number protection (prevents splitting numbers like "3.14").
|
||||
|
||||
See `MULTILINE_IMPLEMENTATION_NOTES.md` for implementation details and recent changes.
|
||||
|
||||
#### Fully Supported Cases
|
||||
|
||||
These atom types work perfectly with interatom line breaking:
|
||||
|
||||
**✅ Variables and ordinary text:**
|
||||
```swift
|
||||
label.latex = "a b c d e f g h i j k l m n o p"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Breaks between individual variables at natural boundaries
|
||||
```
|
||||
|
||||
**✅ Binary operators (+, -, ×, ÷):**
|
||||
```swift
|
||||
label.latex = "a+b+c+d+e+f+g+h"
|
||||
label.preferredMaxLayoutWidth = 100
|
||||
// Breaks cleanly: "a+b+c+d+"
|
||||
// "e+f+g+h"
|
||||
```
|
||||
|
||||
**✅ Relations (=, <, >, ≤, ≥, etc.):**
|
||||
```swift
|
||||
label.latex = "a=1, b=2, c=3, d=4, e=5"
|
||||
label.preferredMaxLayoutWidth = 120
|
||||
// Breaks after commas and operators
|
||||
```
|
||||
|
||||
**✅ Mixed text and simple math:**
|
||||
```swift
|
||||
label.latex = "\\text{Calculer }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Breaks between text and math atoms naturally
|
||||
```
|
||||
|
||||
**✅ Punctuation (commas, periods):**
|
||||
```swift
|
||||
label.latex = "\\text{First, second, third, fourth, fifth}"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Breaks at commas and spaces
|
||||
```
|
||||
|
||||
**✅ Brackets and parentheses (simple):**
|
||||
```swift
|
||||
label.latex = "(a+b)+(c+d)+(e+f)"
|
||||
label.preferredMaxLayoutWidth = 120
|
||||
// Breaks between parenthesized groups
|
||||
```
|
||||
|
||||
**✅ Greek letters and symbols:**
|
||||
```swift
|
||||
label.latex = "\\alpha+\\beta+\\gamma+\\delta+\\epsilon+\\zeta"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Breaks between Greek letters
|
||||
```
|
||||
|
||||
**✅ Fractions (NEW!):**
|
||||
```swift
|
||||
label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Fractions stay inline if they fit, break to new line only when needed
|
||||
// Example: "a + ½ + b" stays on one line if it fits
|
||||
```
|
||||
|
||||
**✅ Radicals/Square roots (NEW!):**
|
||||
```swift
|
||||
label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Radicals stay inline if they fit, break to new line only when needed
|
||||
// Example: "x + √2 + y" stays on one line if it fits
|
||||
```
|
||||
|
||||
**✅ Mixed fractions and radicals (NEW!):**
|
||||
```swift
|
||||
label.latex = "a+\\frac{1}{2}+\\sqrt{3}+b"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Breaks between complex mathematical elements
|
||||
```
|
||||
|
||||
#### Limited Support Cases
|
||||
|
||||
These cases work but with some constraints:
|
||||
|
||||
**✅ Large operators (NEW!):**
|
||||
```swift
|
||||
label.latex = "a + \\sum_{i=1}^{n} x_i + \\int_{0}^{1} f(x)dx + b"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Large operators stay inline when they fit!
|
||||
// Includes height checking for operators with limits
|
||||
```
|
||||
|
||||
**✅ Delimited expressions (NEW!):**
|
||||
```swift
|
||||
label.latex = "(a+b) + \\left(\\frac{c}{d}\\right) + e"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Delimiters stay inline when they fit!
|
||||
// Inner content respects width constraints and wraps naturally
|
||||
```
|
||||
|
||||
**✅ Colored expressions (NEW!):**
|
||||
```swift
|
||||
label.latex = "a + \\color{red}{b + c + d} + e"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Colored sections stay inline when they fit!
|
||||
// Inner content respects width constraints and wraps properly
|
||||
```
|
||||
|
||||
**✅ Matrices/tables (NEW!):**
|
||||
```swift
|
||||
label.latex = "A = \\begin{pmatrix} 1 & 2 \\end{pmatrix} + B"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Small matrices stay inline when they fit!
|
||||
```
|
||||
|
||||
**✅ Atoms with superscripts/subscripts (IMPROVED!):**
|
||||
```swift
|
||||
label.latex = "a^{2}+b^{2}+c^{2}+d^{2}+e^{2}"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Now works with width-based breaking!
|
||||
// Scripted atoms participate in smart line breaking decisions
|
||||
```
|
||||
|
||||
**✅ Math accents:**
|
||||
```swift
|
||||
label.latex = "\\hat{x} + \\tilde{y} + \\bar{z} + \\vec{w}"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// Common accents (\hat, \tilde, \bar, \vec) work well
|
||||
// Vector arrows (\overrightarrow, etc.) supported with stretching
|
||||
```
|
||||
|
||||
**⚠️ Very long single text atoms:**
|
||||
```swift
|
||||
label.latex = "\\text{This is an extremely long piece of text within a single text command}"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// Uses Unicode word boundary breaking with Core Text
|
||||
// Protects numbers from being split (e.g., "3.14" stays together)
|
||||
```
|
||||
**Note**: Breaks within the text atom rather than between atoms, which is expected behavior for very long continuous text.
|
||||
|
||||
#### Best Practices
|
||||
|
||||
**DO:**
|
||||
- Use interatom breaking for simple equations with operators and relations
|
||||
- Use for mixed text and math where you want natural breaks
|
||||
- Use for long sequences of variables, numbers, and operators
|
||||
- Set appropriate `preferredMaxLayoutWidth` based on your layout needs
|
||||
|
||||
**DON'T:**
|
||||
- Expect natural breaking in expressions with large operators (∑, ∫, etc. - not yet optimized)
|
||||
- Expect natural breaking in expressions with \left...\right delimiters (not yet optimized)
|
||||
- Use extremely narrow widths (less than ~80pt) which may cause poor breaks
|
||||
|
||||
#### Examples
|
||||
|
||||
**Excellent use case (discriminant formula):**
|
||||
```swift
|
||||
label.latex = "\\text{Calculer le discriminant }\\Delta=b^{2}-4ac\\text{ avec }a=1\\text{, }b=-1\\text{, }c=-5"
|
||||
label.preferredMaxLayoutWidth = 235
|
||||
// ✅ Breaks naturally at good points between atoms
|
||||
```
|
||||
|
||||
**Good use case (simple arithmetic):**
|
||||
```swift
|
||||
label.latex = "5+10+15+20+25+30+35+40+45+50"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// ✅ Breaks between operators cleanly
|
||||
```
|
||||
|
||||
**Excellent use case (fractions inline - NEW!):**
|
||||
```swift
|
||||
label.latex = "a+\\frac{1}{2}+b+\\frac{3}{4}+c"
|
||||
label.preferredMaxLayoutWidth = 200
|
||||
// ✅ Fractions stay inline when they fit!
|
||||
// Breaks: "a + ½ + b" on line 1, "+ ¾ + c" on line 2
|
||||
```
|
||||
|
||||
**Excellent use case (radicals inline - NEW!):**
|
||||
```swift
|
||||
label.latex = "x+\\sqrt{2}+y+\\sqrt{3}+z"
|
||||
label.preferredMaxLayoutWidth = 150
|
||||
// ✅ Radicals stay inline when they fit!
|
||||
// Example: "x + √2 + y" on line 1, "+ √3 + z" on line 2
|
||||
```
|
||||
|
||||
**Alternative for complex expressions:**
|
||||
```swift
|
||||
// Instead of trying to break this:
|
||||
label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}"
|
||||
// Consider it as a single display equation without width constraint
|
||||
label.preferredMaxLayoutWidth = 0 // No breaking
|
||||
```
|
||||
|
||||
#### Technical Details
|
||||
|
||||
- **Line spacing**: Dynamic line height based on actual content (tall fractions get more space, regular content stays compact)
|
||||
- **Breaking algorithm**: Greedy with look-ahead and break quality scoring - prefers breaking after operators over other positions
|
||||
- **Width calculation**: Includes inter-element spacing according to TeX spacing rules
|
||||
- **Number protection**: Numbers in patterns like "3.14", "1,000", etc. are kept intact
|
||||
- **Supports locales**: English, French, Swiss number formats
|
||||
- **Advanced features**: Tokenization infrastructure with phase-based processing (preprocessing → tokenization → line fitting → display generation)
|
||||
|
||||
For complete implementation details including recent improvements (dynamic line height, break quality scoring, early exit optimization), see [MULTILINE_IMPLEMENTATION_NOTES.md](MULTILINE_IMPLEMENTATION_NOTES.md).
|
||||
|
||||
### Included Features
|
||||
This is a list of formula types that the library currently supports:
|
||||
|
||||
* Simple algebraic equations
|
||||
* Fractions and continued fractions (including `\frac`, `\dfrac`, `\tfrac`, `\cfrac`)
|
||||
* Exponents and subscripts
|
||||
* Trigonometric formulae (including inverse hyperbolic: `\arcsinh`, `\arccosh`, etc.)
|
||||
* Square roots and n-th roots
|
||||
* Calculus symbols - limits, derivatives, integrals (including `\iint`, `\iiint`, `\iiiint`)
|
||||
* Big operators (e.g. product, sum)
|
||||
* Big delimiters (using `\left` and `\right`)
|
||||
* Manual delimiter sizing (`\big`, `\Big`, `\bigg`, `\Bigg` and variants)
|
||||
* Greek alphabet
|
||||
* Bold Greek symbols (`\boldsymbol`)
|
||||
* Combinatorics (`\binom`, `\choose` etc.)
|
||||
* Geometry symbols (e.g. angle, congruence etc.)
|
||||
* Ratios, proportions, percentages
|
||||
* Math spacing
|
||||
* Overline and underline
|
||||
* Math accents (including `\hat`, `\tilde`, `\bar`, `\vec`, `\dot`, `\ddot`, etc.)
|
||||
* Vector arrows (`\vec`, `\overrightarrow`, `\overleftarrow`, `\overleftrightarrow` with automatic stretching)
|
||||
* Wide accents (`\widehat`, `\widetilde`)
|
||||
* Operator names (`\operatorname{name}`)
|
||||
* Matrices (including `\smallmatrix` and starred variants like `pmatrix*` with alignment)
|
||||
* Multi-line subscripts and limits (`\substack`)
|
||||
* Equation alignment
|
||||
* Change bold, roman, caligraphic and other font styles (`\bf`, `\text`, etc.)
|
||||
* Style commands (`\displaystyle`, `\textstyle`)
|
||||
* Custom operators (`\operatorname`, `\operatorname*`)
|
||||
* Dirac notation (`\bra`, `\ket`, `\braket`)
|
||||
* Most commonly used math symbols
|
||||
* Colors for both text and background
|
||||
* **Inline and display math mode delimiters** (see below)
|
||||
* **Automatic line wrapping** (see Automatic Line Wrapping section)
|
||||
|
||||
### LaTeX Math Delimiters
|
||||
|
||||
`SwiftMath` now supports all standard LaTeX math delimiters for both inline and display modes. The parser automatically detects and handles these delimiters:
|
||||
|
||||
#### Inline Math (Text Style)
|
||||
Use these delimiters for inline math within text, which renders more compactly:
|
||||
|
||||
```swift
|
||||
// Dollar signs (TeX style)
|
||||
label.latex = "$E = mc^2$"
|
||||
|
||||
// Parentheses (LaTeX style)
|
||||
label.latex = "\\(\\sum_{i=1}^{n} x_i\\)"
|
||||
|
||||
// Cases environment in inline mode
|
||||
label.latex = "\\(\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}\\)"
|
||||
```
|
||||
|
||||
#### Display Math (Display Style)
|
||||
Use these delimiters for standalone equations with larger operators and limits:
|
||||
|
||||
```swift
|
||||
// Double dollar signs (TeX style)
|
||||
label.latex = "$$\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$"
|
||||
|
||||
// Square brackets (LaTeX style)
|
||||
label.latex = "\\[\\sum_{k=1}^{n} k^2 = \\frac{n(n+1)(2n+1)}{6}\\]"
|
||||
|
||||
// Equation environment
|
||||
label.latex = "\\begin{equation} x^2 + y^2 = z^2 \\end{equation}"
|
||||
|
||||
// Cases environment in display mode
|
||||
label.latex = "\\begin{cases} x + y = 5 \\\\ 2x - y = 1 \\end{cases}"
|
||||
```
|
||||
|
||||
**Note:** The difference between inline and display modes:
|
||||
- **Inline mode** (`$...$` or `\(...\)`) renders compactly, suitable for math within text
|
||||
- **Display mode** (`$$...$$`, `\[...\]`, or environments) renders with larger operators and limits positioned above/below
|
||||
|
||||
All delimiters are automatically stripped during parsing, and the math mode is set appropriately. No additional configuration is needed!
|
||||
|
||||
#### Backward Compatibility
|
||||
Equations without explicit delimiters continue to work as before, defaulting to display mode:
|
||||
|
||||
```swift
|
||||
label.latex = "x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}" // Works as always
|
||||
```
|
||||
|
||||
#### Programmatic API
|
||||
For advanced use cases where you need to parse LaTeX and determine the detected style programmatically, use the `buildWithStyle` method:
|
||||
|
||||
```swift
|
||||
// Parse LaTeX and get both the math list and detected style
|
||||
let (mathList, style) = MTMathListBuilder.buildWithStyle(fromString: "\\[x^2 + y^2 = z^2\\]")
|
||||
|
||||
// style will be .display for \[...\] or $$...$$
|
||||
// style will be .text for \(...\) or $...$
|
||||
|
||||
// Create a display with the detected style
|
||||
if let mathList = mathList {
|
||||
let display = MTTypesetter.createLineForMathList(mathList, font: myFont, style: style)
|
||||
// Use the display for rendering
|
||||
}
|
||||
```
|
||||
|
||||
This is particularly useful when building custom renderers or when you need to respect the user's choice of delimiter style.
|
||||
|
||||
Note: SwiftMath only supports the commands in LaTeX's math mode. There is
|
||||
also no language support for other than west European langugages and some
|
||||
Cyrillic characters. There would be two ways to support more languages:
|
||||
|
||||
1) Find a math font compatible with `SwiftMath` that contains all the glyphs
|
||||
for that language.
|
||||
2) Add support to `SwiftMath` for standard Unicode fonts that contain all
|
||||
langauge glyphs.
|
||||
|
||||
Of these two, the first is much easier. However, if you want a challenge,
|
||||
try to tackle the second option.
|
||||
|
||||
### Example
|
||||
|
||||
The [SwiftMathDemo](https://github.com/mgriebling/SwiftMathDemo) is a SwiftUI version
|
||||
of the Objective-C demo included in `iosMath` that uses `SwiftMath` as a Swift package dependency.
|
||||
|
||||
### Advanced configuration
|
||||
|
||||
`MTMathUILabel` supports some advanced configuration options:
|
||||
|
||||
##### Math mode
|
||||
|
||||
You can change the mode of the `MTMathUILabel` between Display Mode
|
||||
(equivalent to `$$` or `\[` in LaTeX) and Text Mode (equivalent to `$`
|
||||
or `\(` in LaTeX). The default style is Display. To switch to Text
|
||||
simply:
|
||||
|
||||
```swift
|
||||
label.labelMode = .text
|
||||
```
|
||||
|
||||
##### Text Alignment
|
||||
The default alignment of the equations is left. This can be changed to
|
||||
center or right as follows:
|
||||
|
||||
```swift
|
||||
label.textAlignment = .center
|
||||
```
|
||||
|
||||
##### Font size
|
||||
The default font-size is 30pt. You can change it as follows:
|
||||
|
||||
```swift
|
||||
label.fontSize = 25
|
||||
```
|
||||
##### Font
|
||||
The default font is *Latin Modern Math*. This can be changed as:
|
||||
|
||||
```swift
|
||||
label.font = MTFontManager.fontmanager.termesFont(withSize:20)
|
||||
```
|
||||
|
||||
This project has 12 fonts bundled with it, but you can use any OTF math
|
||||
font. A python script is included that generates the `.plist` files
|
||||
required for an `.otf` font to work with `SwiftMath`. If you generate
|
||||
(and test) any other fonts please contribute them back to this project for
|
||||
others to benefit.
|
||||
|
||||
|
||||
|
||||
Note: The `KpMath-Light`, `KpMath-Sans`, `Asana` fonts currently incorrectly
|
||||
render very large radicals. It appears that the font files do
|
||||
not properly define the offsets required to typeset these glyphs. If
|
||||
anyone can fix this, it would be greatly appreciated.
|
||||
|
||||
##### Text Color
|
||||
The default color of the rendered equation is black. You can change
|
||||
it to any other color as follows:
|
||||
|
||||
```swift
|
||||
label.textColor = .red
|
||||
```
|
||||
|
||||
It is also possible to set different colors for different parts of the
|
||||
equation. Just access the `displayList` field and set the `textColor`
|
||||
of the underlying displays of which you want to change the color.
|
||||
|
||||
##### Fallback Font for Unicode Text
|
||||
By default, math fonts only support a limited set of characters (Latin, Greek, common math symbols).
|
||||
To display other Unicode characters like Chinese, Japanese, Korean, emoji, or other scripts in `\text{}`
|
||||
commands, you can configure a fallback font:
|
||||
|
||||
```swift
|
||||
let mathFont = MTFontManager().font(withName: MathFont.latinModernFont.rawValue, size: 30)
|
||||
|
||||
// Set a fallback font for unsupported characters (defaults to nil)
|
||||
#if os(iOS) || os(visionOS)
|
||||
let systemFont = UIFont.systemFont(ofSize: 30)
|
||||
mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil)
|
||||
#elseif os(macOS)
|
||||
let systemFont = NSFont.systemFont(ofSize: 30)
|
||||
mathFont?.fallbackFont = CTFontCreateWithName(systemFont.fontName as CFString, 30, nil)
|
||||
#endif
|
||||
|
||||
label.font = mathFont
|
||||
label.latex = "\\text{Hello 世界 🌍}" // English, Chinese, and emoji
|
||||
```
|
||||
|
||||
When the main math font doesn't contain a glyph for a character, the fallback font will be used automatically.
|
||||
This is particularly useful for:
|
||||
- Chinese text: `\text{中文}`
|
||||
- Japanese text: `\text{日本語}`
|
||||
- Korean text: `\text{한국어}`
|
||||
- Emoji: `\text{Math is fun! 🎉📐}`
|
||||
- Mixed scripts: `\text{Equation: 方程式}`
|
||||
|
||||
**Note**: The fallback font only applies to characters within `\text{}` commands, not regular math mode.
|
||||
|
||||
##### Custom Commands
|
||||
You can define your own commands that are not already predefined. This is
|
||||
similar to macros is LaTeX. To define your own command use:
|
||||
|
||||
```swift
|
||||
MTMathAtomFactory.addLatexSymbol("lcm", value: MTMathAtomFactory.operator(withName: "lcm", limits: false))
|
||||
```
|
||||
|
||||
This creates an `\lcm` command that can be used in the LaTeX.
|
||||
|
||||
##### Content Insets
|
||||
The `MTMathUILabel` has `contentInsets` for finer control of placement of the
|
||||
equation in relation to the view.
|
||||
|
||||
If you need to set it you can do as follows:
|
||||
|
||||
```swift
|
||||
label.contentInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 20)
|
||||
```
|
||||
|
||||
##### Error handling
|
||||
|
||||
If the LaTeX text given to `MTMathUILabel` is
|
||||
invalid or if it contains commands that aren't currently supported then
|
||||
an error message will be displayed instead of the label.
|
||||
|
||||
This error can be programmatically retrieved as `label.error`. If you
|
||||
prefer not to display anything then set:
|
||||
|
||||
```swift
|
||||
label.displayErrorInline = true
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Note this is not a complete implementation of LaTeX math mode. There are
|
||||
some pieces that are missing and may be included in future updates:
|
||||
|
||||
* `\middle` delimiter for use between `\left` and `\right`
|
||||
* Some fine spacing commands (`\:`, `\;`, `\!` - note that `\,` works)
|
||||
|
||||
For a complete list of features and their implementation status, see [MISSING_FEATURES.md](MISSING_FEATURES.md).
|
||||
|
||||
## License
|
||||
|
||||
`SwiftMath` is available under the MIT license. See the [LICENSE](./LICENSE)
|
||||
file for more info.
|
||||
|
||||
### Fonts
|
||||
This distribution contains the following fonts. These fonts are
|
||||
licensed as follows:
|
||||
* Latin Modern Math:
|
||||
[GUST Font License](GUST-FONT-LICENSE.txt)
|
||||
* Tex Gyre Termes:
|
||||
[GUST Font License](GUST-FONT-LICENSE.txt)
|
||||
* [XITS Math](https://github.com/khaledhosny/xits-math):
|
||||
[Open Font License](OFL.txt)
|
||||
* [KpMath Light/KpMath Sans](http://scripts.sil.org/OFL):
|
||||
[SIL Open Font License](OFL.txt)
|
||||
194
third-party/SwiftMath/Sources/MathBundle/MTFontMathTableV2.swift
vendored
Executable file
194
third-party/SwiftMath/Sources/MathBundle/MTFontMathTableV2.swift
vendored
Executable file
|
|
@ -0,0 +1,194 @@
|
|||
//
|
||||
// MTFontMathTableV2.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 15/9/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import CoreText
|
||||
|
||||
internal class MTFontMathTableV2: MTFontMathTable {
|
||||
private let mathFont: MathFont
|
||||
private let fontSize: CGFloat
|
||||
private let unitsPerEm: UInt
|
||||
private let mTable: NSDictionary
|
||||
init(mathFont: MathFont, size: CGFloat, unitsPerEm: UInt) {
|
||||
self.mathFont = mathFont
|
||||
self.fontSize = size
|
||||
self.unitsPerEm = unitsPerEm
|
||||
mTable = mathFont.rawMathTable()
|
||||
super.init(withFont: mathFont.mtfont(size: fontSize), mathTable: mTable)
|
||||
super._mathTable = nil
|
||||
// disable all possible access to _mathTable in superclass!
|
||||
}
|
||||
override var _mathTable: NSDictionary? {
|
||||
set { fatalError("\(#function) change to _mathTable \(mathFont.rawValue) not allowed.") }
|
||||
get { mTable }
|
||||
}
|
||||
override var muUnit: CGFloat { fontSize/18 }
|
||||
|
||||
override func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
|
||||
CGFloat(fontUnits) * fontSize / CGFloat(unitsPerEm)
|
||||
}
|
||||
override func constantFromTable(_ constName: String) -> CGFloat {
|
||||
guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[constName] as? NSNumber else {
|
||||
return .zero
|
||||
}
|
||||
return fontUnitsToPt(val.intValue)
|
||||
}
|
||||
override func percentFromTable(_ percentName: String) -> CGFloat {
|
||||
guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[percentName] as? NSNumber else {
|
||||
return .zero
|
||||
}
|
||||
return CGFloat(val.floatValue) / 100
|
||||
}
|
||||
/** Returns an Array of all the vertical variants of the glyph if any. If
|
||||
there are no variants for the glyph, the array contains the given glyph. */
|
||||
override func getVerticalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] {
|
||||
guard let variants = mTable[kVertVariants] as? NSDictionary else { return [] }
|
||||
return self.getVariantsForGlyph(glyph, inDictionary: variants)
|
||||
}
|
||||
/** Returns an Array of all the horizontal variants of the glyph if any. If
|
||||
there are no variants for the glyph, the array contains the given glyph. */
|
||||
override func getHorizontalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] {
|
||||
guard let variants = mTable[kHorizVariants] as? NSDictionary else { return [] }
|
||||
return self.getVariantsForGlyph(glyph, inDictionary:variants)
|
||||
}
|
||||
override func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants: NSDictionary) -> [NSNumber?] {
|
||||
let font = mathFont.mtfont(size: fontSize)
|
||||
let glyphName = font.get(nameForGlyph: glyph)
|
||||
|
||||
var glyphArray = [NSNumber]()
|
||||
let variantGlyphs = variants[glyphName] as? NSArray
|
||||
|
||||
guard let variantGlyphs = variantGlyphs, variantGlyphs.count != .zero else {
|
||||
// There are no extra variants, so just add the current glyph to it.
|
||||
let glyph = font.get(glyphWithName: glyphName)
|
||||
glyphArray.append(NSNumber(value:glyph))
|
||||
return glyphArray
|
||||
}
|
||||
for gvn in variantGlyphs {
|
||||
if let glyphVariantName = gvn as? String {
|
||||
let variantGlyph = font.get(glyphWithName: glyphVariantName)
|
||||
glyphArray.append(NSNumber(value:variantGlyph))
|
||||
}
|
||||
}
|
||||
return glyphArray
|
||||
}
|
||||
/** Returns a larger vertical variant of the given glyph if any.
|
||||
If there is no larger version, this returns the current glyph.
|
||||
|
||||
- Parameter glyph: The glyph to find a larger variant for
|
||||
- Parameter forDisplayStyle: If true, selects the largest appropriate variant for display style.
|
||||
If false, selects the next larger variant (incremental sizing).
|
||||
- Returns: A larger glyph variant, or the original glyph if no variants exist
|
||||
*/
|
||||
override func getLargerGlyph(_ glyph: CGGlyph, forDisplayStyle: Bool = false) -> CGGlyph {
|
||||
let font = mathFont.mtfont(size: fontSize)
|
||||
let glyphName = font.get(nameForGlyph: glyph)
|
||||
|
||||
guard let variants = mTable[kVertVariants] as? NSDictionary,
|
||||
let variantGlyphs = variants[glyphName] as? NSArray, variantGlyphs.count != .zero else {
|
||||
// There are no extra variants, so just returnt the current glyph.
|
||||
return glyph
|
||||
}
|
||||
|
||||
if forDisplayStyle {
|
||||
// For display style, select a large variant suitable for mathematical display mode
|
||||
// Display integrals should be significantly larger (~2.2em) for visual prominence
|
||||
let count = variantGlyphs.count
|
||||
|
||||
// Strategy: Use the largest variant, but avoid extreme sizes for fonts with many variants
|
||||
let targetIndex: Int
|
||||
if count <= 2 {
|
||||
// Small variant list: use the last one (e.g., integral.size1 at ~2.2em)
|
||||
targetIndex = count - 1
|
||||
} else if count <= 4 {
|
||||
// Medium variant list: use second-to-last to avoid extremes
|
||||
targetIndex = count - 2
|
||||
} else {
|
||||
// Large variant list (like texgyretermes with 6 variants):
|
||||
// Use variant at ~60% position to get appropriate display size (~2.0em)
|
||||
// For 7 variants (0-6), this gives index 4
|
||||
targetIndex = min(count - 2, Int(Double(count) * 0.6))
|
||||
}
|
||||
|
||||
if let glyphVariantName = variantGlyphs[targetIndex] as? String {
|
||||
let variantGlyph = font.get(glyphWithName: glyphVariantName)
|
||||
return variantGlyph
|
||||
}
|
||||
} else {
|
||||
// Text/inline style: use incremental sizing for moderate enlargement
|
||||
// Find the first variant with a different name
|
||||
for gvn in variantGlyphs {
|
||||
if let glyphVariantName = gvn as? String, glyphVariantName != glyphName {
|
||||
let variantGlyph = font.get(glyphWithName: glyphVariantName)
|
||||
return variantGlyph
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We did not find any variants of this glyph so return it.
|
||||
return glyph
|
||||
}
|
||||
/** Returns the italic correction for the given glyph if any. If there
|
||||
isn't any this returns 0. */
|
||||
override func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat {
|
||||
let font = mathFont.mtfont(size: fontSize)
|
||||
let glyphName = font.get(nameForGlyph: glyph)
|
||||
|
||||
guard let italics = mTable[kItalic] as? NSDictionary, let val = italics[glyphName] as? NSNumber else {
|
||||
return .zero
|
||||
}
|
||||
// if val is nil, this returns 0.
|
||||
return fontUnitsToPt(val.intValue)
|
||||
}
|
||||
override func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat {
|
||||
let font = mathFont.mtfont(size: fontSize)
|
||||
let glyphName = font.get(nameForGlyph: glyph)
|
||||
|
||||
guard let accents = mTable[kAccents] as? NSDictionary, let val = accents[glyphName] as? NSNumber else {
|
||||
// If no top accent is defined then it is the center of the advance width.
|
||||
var glyph = glyph
|
||||
var advances = CGSize.zero
|
||||
CTFontGetAdvancesForGlyphs(font.ctFont, .horizontal, &glyph, &advances, 1)
|
||||
return advances.width/2
|
||||
}
|
||||
return fontUnitsToPt(val.intValue)
|
||||
}
|
||||
override func getVerticalGlyphAssembly(forGlyph glyph: CGGlyph) -> [GlyphPart] {
|
||||
let font = mathFont.mtfont(size: fontSize)
|
||||
let glyphName = font.get(nameForGlyph: glyph)
|
||||
|
||||
guard let assemblyTable = mTable[kVertAssembly] as? NSDictionary,
|
||||
let assemblyInfo = assemblyTable[glyphName] as? NSDictionary,
|
||||
let parts = assemblyInfo[kAssemblyParts] as? NSArray else {
|
||||
// No vertical assembly defined for glyph
|
||||
// parts should always have been defined, but if it isn't return nil
|
||||
return []
|
||||
}
|
||||
|
||||
var rv = [GlyphPart]()
|
||||
for part in parts {
|
||||
guard let partInfo = part as? NSDictionary,
|
||||
let adv = partInfo["advance"] as? NSNumber,
|
||||
let end = partInfo["endConnector"] as? NSNumber,
|
||||
let start = partInfo["startConnector"] as? NSNumber,
|
||||
let ext = partInfo["extender"] as? NSNumber,
|
||||
let glyphName = partInfo["glyph"] as? String else { continue }
|
||||
let fullAdvance = fontUnitsToPt(adv.intValue)
|
||||
let endConnectorLength = fontUnitsToPt(end.intValue)
|
||||
let startConnectorLength = fontUnitsToPt(start.intValue)
|
||||
let isExtender = ext.boolValue
|
||||
let glyph = font.get(glyphWithName: glyphName)
|
||||
let part = GlyphPart(glyph: glyph, fullAdvance: fullAdvance,
|
||||
startConnectorLength: startConnectorLength,
|
||||
endConnectorLength: endConnectorLength,
|
||||
isExtender: isExtender)
|
||||
rv.append(part)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
}
|
||||
68
third-party/SwiftMath/Sources/MathBundle/MTFontV2.swift
vendored
Executable file
68
third-party/SwiftMath/Sources/MathBundle/MTFontV2.swift
vendored
Executable file
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// MTFontV2.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 15/9/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import CoreText
|
||||
|
||||
extension MathFont {
|
||||
public func mtfont(size: CGFloat) -> MTFontV2 {
|
||||
MTFontV2(font: self, size: size)
|
||||
}
|
||||
}
|
||||
public final class MTFontV2: MTFont {
|
||||
let font: MathFont
|
||||
let size: CGFloat
|
||||
private let _cgFont: CGFont
|
||||
private let _ctFont: CTFont
|
||||
private let unitsPerEm: UInt
|
||||
private var _mathTab: MTFontMathTableV2?
|
||||
init(font: MathFont = .latinModernFont, size: CGFloat) {
|
||||
self.font = font
|
||||
self.size = size
|
||||
// MathFont cgfont and ctfont are fast & threadsafe, keep a local copy is cheaper than
|
||||
// handling via NSLock
|
||||
self._cgFont = font.cgFont()
|
||||
self._ctFont = font.ctFont(withSize: size)
|
||||
self.unitsPerEm = self._ctFont.unitsPerEm
|
||||
super.init()
|
||||
|
||||
super.defaultCGFont = nil
|
||||
super.ctFont = nil
|
||||
super.mathTable = nil
|
||||
super.rawMathTable = nil
|
||||
}
|
||||
override var defaultCGFont: CGFont! {
|
||||
set { fatalError("\(#function): change to \(font.fontName) not allowed.") }
|
||||
get { _cgFont }
|
||||
}
|
||||
override var ctFont: CTFont! {
|
||||
set { fatalError("\(#function): change to \(font.fontName) not allowed.") }
|
||||
get { _ctFont }
|
||||
}
|
||||
private let mtfontV2LockOnMathTable = NSLock()
|
||||
override var mathTable: MTFontMathTable? {
|
||||
set { fatalError("\(#function): change to \(font.rawValue) not allowed.") }
|
||||
get {
|
||||
guard _mathTab == nil else { return _mathTab }
|
||||
//Note: lazy _mathTab initialization is now threadsafe.
|
||||
mtfontV2LockOnMathTable.lock()
|
||||
defer { mtfontV2LockOnMathTable.unlock() }
|
||||
if _mathTab == nil {
|
||||
_mathTab = MTFontMathTableV2(mathFont: font, size: size, unitsPerEm: unitsPerEm)
|
||||
}
|
||||
return _mathTab
|
||||
}
|
||||
}
|
||||
override var rawMathTable: NSDictionary? {
|
||||
set { fatalError("\(#function): change to \(font.rawValue) not allowed.") }
|
||||
get { fatalError("\(#function): access to \(font.rawValue) not allowed.") }
|
||||
}
|
||||
public override func copy(withSize size: CGFloat) -> MTFont {
|
||||
MTFontV2(font: font, size: size)
|
||||
}
|
||||
}
|
||||
225
third-party/SwiftMath/Sources/MathBundle/MathFont.swift
vendored
Normal file
225
third-party/SwiftMath/Sources/MathBundle/MathFont.swift
vendored
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
//
|
||||
// MathFont.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 10/9/2023.
|
||||
//
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
/// Now available for everyone to use
|
||||
public enum MathFont: String, CaseIterable, Identifiable {
|
||||
|
||||
public var id: Self { self } // Makes things simpler for SwiftUI
|
||||
|
||||
case latinModernFont = "latinmodern-math"
|
||||
case kpMathLightFont = "KpMath-Light"
|
||||
case kpMathSansFont = "KpMath-Sans"
|
||||
case xitsFont = "xits-math"
|
||||
case termesFont = "texgyretermes-math"
|
||||
case asanaFont = "Asana-Math"
|
||||
case eulerFont = "Euler-Math"
|
||||
case firaFont = "FiraMath-Regular"
|
||||
case notoSansFont = "NotoSansMath-Regular"
|
||||
case libertinusFont = "LibertinusMath-Regular"
|
||||
case garamondFont = "Garamond-Math"
|
||||
case leteSansFont = "LeteSansMath"
|
||||
|
||||
var fontFamilyName: String {
|
||||
switch self {
|
||||
case .latinModernFont: "Latin Modern Math"
|
||||
case .kpMathLightFont: "KpMath"
|
||||
case .kpMathSansFont: "KpMath"
|
||||
case .xitsFont: "XITS Math"
|
||||
case .termesFont: "TeX Gyre Termes Math"
|
||||
case .asanaFont: "Asana Math"
|
||||
case .eulerFont: "Euler Math"
|
||||
case .firaFont: "Fira Math"
|
||||
case .notoSansFont: "Noto Sans Math"
|
||||
case .libertinusFont: "Libertinus Math"
|
||||
case .garamondFont: "Garamond-Math" // PostScript name is "Garamond-Math", not "Garamond Math"
|
||||
case .leteSansFont: "Lete Sans Math"
|
||||
}
|
||||
}
|
||||
|
||||
var postScriptName: String {
|
||||
switch self {
|
||||
case .latinModernFont: "LatinModernMath-Regular"
|
||||
case .kpMathLightFont: "KpMath-Light"
|
||||
case .kpMathSansFont: "KpMath-Sans"
|
||||
case .xitsFont: "XITSMath"
|
||||
case .termesFont: "TeXGyreTermesMath-Regular"
|
||||
case .asanaFont: "Asana-Math"
|
||||
case .eulerFont: "Euler-Math"
|
||||
case .firaFont: "FiraMath-Regular"
|
||||
case .notoSansFont: "NotoSansMath-Regular"
|
||||
case .libertinusFont: "LibertinusMath-Regular"
|
||||
case .garamondFont: "Garamond-Math"
|
||||
case .leteSansFont: "LeteSansMath"
|
||||
}
|
||||
}
|
||||
|
||||
var fontName: String { self.rawValue }
|
||||
|
||||
public func cgFont() -> CGFont {
|
||||
BundleManager.manager.obtainCGFont(font: self)
|
||||
}
|
||||
public func ctFont(withSize size: CGFloat) -> CTFont {
|
||||
BundleManager.manager.obtainCTFont(font: self, withSize: size)
|
||||
}
|
||||
internal func rawMathTable() -> NSDictionary {
|
||||
BundleManager.manager.obtainRawMathTable(font: self)
|
||||
}
|
||||
|
||||
//Note: Below code are no longer supported, unable to tell if UIFont/NSFont is threadsafe, not used in SwiftMath.
|
||||
// #if os(iOS) || os(visionOS)
|
||||
// public func uiFont(withSize size: CGFloat) -> UIFont? {
|
||||
// UIFont(name: fontName, size: size)
|
||||
// }
|
||||
// #endif
|
||||
// #if os(macOS)
|
||||
// public func nsFont(withSize size: CGFloat) -> NSFont? {
|
||||
// NSFont(name: fontName, size: size)
|
||||
// }
|
||||
// #endif
|
||||
}
|
||||
internal extension CTFont {
|
||||
/** The size of this font in points. */
|
||||
var fontSize: CGFloat {
|
||||
CTFontGetSize(self)
|
||||
}
|
||||
var unitsPerEm: UInt {
|
||||
return UInt(CTFontGetUnitsPerEm(self))
|
||||
}
|
||||
}
|
||||
private class BundleManager {
|
||||
//Note: below should be lightweight and without threadsafe problem.
|
||||
static internal let manager = BundleManager()
|
||||
|
||||
private var cgFonts = [MathFont: CGFont]()
|
||||
private var ctFonts = [CTFontSizePair: CTFont]()
|
||||
private var rawMathTables = [MathFont: NSDictionary]()
|
||||
|
||||
private let threadSafeQueue = DispatchQueue(label: "com.smartmath.mathfont.threadsafequeue",
|
||||
qos: .userInitiated,
|
||||
attributes: .concurrent)
|
||||
|
||||
private func registerCGFont(mathFont: MathFont) throws {
|
||||
guard let frameworkBundleURL = Bundle.main.url(forResource: "mathFonts", withExtension: "bundle"),
|
||||
let resourceBundleURL = Bundle(url: frameworkBundleURL)?.path(forResource: mathFont.rawValue, ofType: "otf") else {
|
||||
throw FontError.fontPathNotFound
|
||||
}
|
||||
guard let fontData = NSData(contentsOfFile: resourceBundleURL), let dataProvider = CGDataProvider(data: fontData) else {
|
||||
throw FontError.invalidFontFile
|
||||
}
|
||||
guard let defaultCGFont = CGFont(dataProvider) else {
|
||||
throw FontError.initFontError
|
||||
}
|
||||
|
||||
cgFonts[mathFont] = defaultCGFont
|
||||
|
||||
/// This does not load the complete math font, it only has about half the glyphs of the full math font.
|
||||
/// In particular it does not have the math italic characters which breaks our variable rendering.
|
||||
/// So we first load a CGFont from the file and then convert it to a CTFont.
|
||||
var errorRef: Unmanaged<CFError>? = nil
|
||||
guard CTFontManagerRegisterGraphicsFont(defaultCGFont, &errorRef) else {
|
||||
throw FontError.registerFailed
|
||||
}
|
||||
let postsript = (defaultCGFont.postScriptName as? String) ?? ""
|
||||
let cgfontName = (defaultCGFont.fullName as? String) ?? ""
|
||||
let threadName = Thread.isMainThread ? "main" : "global"
|
||||
debugPrint("mathFonts bundle resource: \(mathFont.rawValue), font: \(cgfontName), ps: \(postsript) registered on \(threadName).")
|
||||
}
|
||||
|
||||
private func registerMathTable(mathFont: MathFont) throws {
|
||||
guard let frameworkBundleURL = Bundle.main.url(forResource: "mathFonts", withExtension: "bundle"),
|
||||
let mathTablePlist = Bundle(url: frameworkBundleURL)?.url(forResource: mathFont.rawValue, withExtension:"plist") else {
|
||||
throw FontError.fontPathNotFound
|
||||
}
|
||||
guard let rawMathTable = NSDictionary(contentsOf: mathTablePlist),
|
||||
let version = rawMathTable["version"] as? String,
|
||||
version == "1.3" else {
|
||||
throw FontError.invalidMathTable
|
||||
}
|
||||
|
||||
rawMathTables[mathFont] = rawMathTable
|
||||
|
||||
let threadName = Thread.isMainThread ? "main" : "global"
|
||||
debugPrint("mathFonts bundle resource: \(mathFont.rawValue).plist registered on \(threadName).")
|
||||
}
|
||||
|
||||
private func onDemandRegistration(mathFont: MathFont) {
|
||||
guard threadSafeQueue.sync(execute: { cgFonts[mathFont] }) == nil else { return }
|
||||
// Note: resourceLoading is now serialized.
|
||||
threadSafeQueue.sync(flags: .barrier, execute: { [weak self] in
|
||||
if self?.cgFonts[mathFont] == nil {
|
||||
do {
|
||||
try BundleManager.manager.registerCGFont(mathFont: mathFont)
|
||||
try BundleManager.manager.registerMathTable(mathFont: mathFont)
|
||||
|
||||
} catch {
|
||||
fatalError("MTMathFonts:\(#function) ondemand loading failed, mathFont \(mathFont.rawValue), reason \(error)")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
fileprivate func obtainCGFont(font: MathFont) -> CGFont {
|
||||
onDemandRegistration(mathFont: font)
|
||||
guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else {
|
||||
fatalError("\(#function) unable to locate CGFont \(font.fontName)")
|
||||
}
|
||||
return cgFont
|
||||
}
|
||||
|
||||
fileprivate func obtainCTFont(font: MathFont, withSize size: CGFloat) -> CTFont {
|
||||
onDemandRegistration(mathFont: font)
|
||||
let fontSizePair = CTFontSizePair(font: font, size: size)
|
||||
let ctFont = threadSafeQueue.sync(execute: { ctFonts[fontSizePair] })
|
||||
guard ctFont == nil else { return ctFont! }
|
||||
guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else {
|
||||
fatalError("\(#function) unable to locate CGFont \(font.fontName) to create CTFont")
|
||||
}
|
||||
//Note: ctfont creation and caching is now threadsafe.
|
||||
guard threadSafeQueue.sync(execute: { ctFonts[fontSizePair] }) == nil else { return ctFonts[fontSizePair]! }
|
||||
return threadSafeQueue.sync(flags: .barrier, execute: {
|
||||
if let ctfont = ctFonts[fontSizePair] {
|
||||
return ctfont
|
||||
} else {
|
||||
let result = CTFontCreateWithGraphicsFont(cgFont, size, nil, nil)
|
||||
ctFonts[fontSizePair] = result
|
||||
return result
|
||||
}
|
||||
})
|
||||
}
|
||||
fileprivate func obtainRawMathTable(font: MathFont) -> NSDictionary {
|
||||
onDemandRegistration(mathFont: font)
|
||||
guard let mathTable = threadSafeQueue.sync(execute: { rawMathTables[font] } ) else {
|
||||
fatalError("\(#function) unable to locate mathTable: \(font.rawValue).plist")
|
||||
}
|
||||
return mathTable
|
||||
}
|
||||
deinit {
|
||||
ctFonts.removeAll()
|
||||
var errorRef: Unmanaged<CFError>? = nil
|
||||
cgFonts.values.forEach { cgFont in
|
||||
CTFontManagerUnregisterGraphicsFont(cgFont, &errorRef)
|
||||
}
|
||||
cgFonts.removeAll()
|
||||
}
|
||||
public enum FontError: Error {
|
||||
case invalidFontFile
|
||||
case fontPathNotFound
|
||||
case initFontError
|
||||
case registerFailed
|
||||
case invalidMathTable
|
||||
}
|
||||
|
||||
private struct CTFontSizePair: Hashable {
|
||||
let font: MathFont
|
||||
let size: CGFloat
|
||||
}
|
||||
}
|
||||
123
third-party/SwiftMath/Sources/MathBundle/MathImage.swift
vendored
Executable file
123
third-party/SwiftMath/Sources/MathBundle/MathImage.swift
vendored
Executable file
|
|
@ -0,0 +1,123 @@
|
|||
//
|
||||
// MathImage.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 15/9/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public struct MathImage {
|
||||
public var font: MathFont = .latinModernFont
|
||||
public var fontSize: CGFloat
|
||||
public var textColor: MTColor
|
||||
|
||||
public var labelMode: MTMathUILabelMode
|
||||
public var textAlignment: MTTextAlignment
|
||||
|
||||
public var contentInsets: MTEdgeInsets = MTEdgeInsetsZero
|
||||
|
||||
public let latex: String
|
||||
|
||||
private(set) var intrinsicContentSize = CGSize.zero
|
||||
|
||||
public init(latex: String, fontSize: CGFloat, textColor: MTColor, labelMode: MTMathUILabelMode = .display, textAlignment: MTTextAlignment = .center) {
|
||||
self.latex = latex
|
||||
self.fontSize = fontSize
|
||||
self.textColor = textColor
|
||||
self.labelMode = labelMode
|
||||
self.textAlignment = textAlignment
|
||||
}
|
||||
}
|
||||
extension MathImage {
|
||||
public var currentStyle: MTLineStyle {
|
||||
switch labelMode {
|
||||
case .display: return .display
|
||||
case .text: return .text
|
||||
}
|
||||
}
|
||||
private func intrinsicContentSize(_ displayList: MTMathListDisplay) -> CGSize {
|
||||
CGSize(width: displayList.width + contentInsets.left + contentInsets.right,
|
||||
height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom)
|
||||
}
|
||||
public struct LayoutInfo {
|
||||
public var ascent: CGFloat = 0
|
||||
public var descent: CGFloat = 0
|
||||
|
||||
public init(ascent: CGFloat, descent: CGFloat) {
|
||||
self.ascent = ascent
|
||||
self.descent = descent
|
||||
}
|
||||
}
|
||||
public mutating func asImage() -> (NSError?, MTImage?, LayoutInfo?) {
|
||||
func layoutImage(size: CGSize, displayList: MTMathListDisplay) {
|
||||
var textX = CGFloat(0)
|
||||
switch self.textAlignment {
|
||||
case .left: textX = contentInsets.left
|
||||
case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
|
||||
case .right: textX = size.width - displayList.width - contentInsets.right
|
||||
}
|
||||
let availableHeight = size.height - contentInsets.bottom - contentInsets.top
|
||||
|
||||
// center things vertically
|
||||
var height = displayList.ascent + displayList.descent
|
||||
if height < fontSize/2 {
|
||||
height = fontSize/2 // set height to half the font size
|
||||
}
|
||||
let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom
|
||||
displayList.position = CGPoint(x: textX, y: textY)
|
||||
}
|
||||
var error: NSError?
|
||||
let mtfont: MTFont? = font.mtfont(size: fontSize)
|
||||
|
||||
guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil,
|
||||
let displayList = MTTypesetter.createLineForMathList(mathList, font: mtfont, style: currentStyle) else {
|
||||
return (error, nil, nil)
|
||||
}
|
||||
|
||||
intrinsicContentSize = intrinsicContentSize(displayList)
|
||||
displayList.textColor = textColor
|
||||
|
||||
let size = intrinsicContentSize.regularized
|
||||
layoutImage(size: size, displayList: displayList)
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
let image = renderer.image { rendererContext in
|
||||
rendererContext.cgContext.saveGState()
|
||||
rendererContext.cgContext.concatenate(.flippedVertically(size.height))
|
||||
displayList.draw(rendererContext.cgContext)
|
||||
rendererContext.cgContext.restoreGState()
|
||||
}
|
||||
return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent))
|
||||
#endif
|
||||
#if os(macOS)
|
||||
let image = NSImage(size: size, flipped: false) { bounds in
|
||||
guard let context = NSGraphicsContext.current?.cgContext else { return false }
|
||||
context.saveGState()
|
||||
displayList.draw(context)
|
||||
context.restoreGState()
|
||||
return true
|
||||
}
|
||||
return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
private extension CGAffineTransform {
|
||||
static func flippedVertically(_ height: CGFloat) -> CGAffineTransform {
|
||||
var transform = CGAffineTransform(scaleX: 1, y: -1)
|
||||
transform = transform.translatedBy(x: 0, y: -height)
|
||||
return transform
|
||||
}
|
||||
}
|
||||
extension CGSize {
|
||||
fileprivate var regularized: CGSize {
|
||||
CGSize(width: ceil(width), height: ceil(height))
|
||||
}
|
||||
}
|
||||
35
third-party/SwiftMath/Sources/MathRender/MTBezierPath.swift
vendored
Executable file
35
third-party/SwiftMath/Sources/MathRender/MTBezierPath.swift
vendored
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by 安志钢.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
extension MTBezierPath {
|
||||
func addLine(to point: CGPoint) {
|
||||
self.line(to: point)
|
||||
}
|
||||
}
|
||||
|
||||
extension MTView {
|
||||
|
||||
var backgroundColor:MTColor? {
|
||||
get {
|
||||
MTColor(cgColor: self.layer?.backgroundColor ?? MTColor.clear.cgColor)
|
||||
}
|
||||
set {
|
||||
self.layer?.backgroundColor = MTColor.clear.cgColor
|
||||
self.wantsLayer = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
28
third-party/SwiftMath/Sources/MathRender/MTColor.swift
vendored
Executable file
28
third-party/SwiftMath/Sources/MathRender/MTColor.swift
vendored
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Markus Sähn.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension MTColor {
|
||||
|
||||
public convenience init?(fromHexString hexString:String) {
|
||||
if hexString.isEmpty { return nil }
|
||||
if !hexString.hasPrefix("#") { return nil }
|
||||
|
||||
var rgbValue = UInt64(0)
|
||||
let scanner = Scanner(string: hexString)
|
||||
scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#")
|
||||
scanner.scanHexInt64(&rgbValue)
|
||||
self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16)/255.0,
|
||||
green: CGFloat((rgbValue & 0xFF00) >> 8)/255.0,
|
||||
blue: CGFloat((rgbValue & 0xFF))/255.0,
|
||||
alpha: 1.0)
|
||||
}
|
||||
|
||||
}
|
||||
40
third-party/SwiftMath/Sources/MathRender/MTConfig.swift
vendored
Executable file
40
third-party/SwiftMath/Sources/MathRender/MTConfig.swift
vendored
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
import Foundation
|
||||
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by 安志钢.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
|
||||
import UIKit
|
||||
|
||||
public typealias MTView = UIView
|
||||
public typealias MTColor = UIColor
|
||||
public typealias MTBezierPath = UIBezierPath
|
||||
public typealias MTLabel = UILabel
|
||||
public typealias MTEdgeInsets = UIEdgeInsets
|
||||
public typealias MTRect = CGRect
|
||||
public typealias MTImage = UIImage
|
||||
|
||||
let MTEdgeInsetsZero = UIEdgeInsets.zero
|
||||
func MTGraphicsGetCurrentContext() -> CGContext? { UIGraphicsGetCurrentContext() }
|
||||
|
||||
#else
|
||||
|
||||
import AppKit
|
||||
|
||||
public typealias MTView = NSView
|
||||
public typealias MTColor = NSColor
|
||||
public typealias MTBezierPath = NSBezierPath
|
||||
public typealias MTEdgeInsets = NSEdgeInsets
|
||||
public typealias MTRect = NSRect
|
||||
public typealias MTImage = NSImage
|
||||
|
||||
let MTEdgeInsetsZero = NSEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0)
|
||||
func MTGraphicsGetCurrentContext() -> CGContext? { NSGraphicsContext.current?.cgContext }
|
||||
|
||||
#endif
|
||||
81
third-party/SwiftMath/Sources/MathRender/MTFont.swift
vendored
Normal file
81
third-party/SwiftMath/Sources/MathRender/MTFont.swift
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
|
||||
import Foundation
|
||||
import CoreText
|
||||
import AppBundle
|
||||
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
public class MTFont {
|
||||
|
||||
var defaultCGFont: CGFont!
|
||||
var ctFont: CTFont!
|
||||
var mathTable: MTFontMathTable?
|
||||
var rawMathTable: NSDictionary?
|
||||
|
||||
/// Fallback font for characters not supported by the main math font.
|
||||
/// Defaults to the system font at the same size. This is particularly useful
|
||||
/// for rendering text in \text{} commands with characters outside the math font's coverage
|
||||
/// (e.g., Chinese, Japanese, Korean, emoji, etc.)
|
||||
public var fallbackFont: CTFont?
|
||||
|
||||
init() {}
|
||||
|
||||
/// `MTFont(fontWithName:)` does not load the complete math font, it only has about half the glyphs of the full math font.
|
||||
/// In particular it does not have the math italic characters which breaks our variable rendering.
|
||||
/// So we first load a CGFont from the file and then convert it to a CTFont.
|
||||
convenience init(fontWithName name: String, size:CGFloat) {
|
||||
self.init()
|
||||
//print("Loading font \(name)")
|
||||
let bundle = getAppBundle()
|
||||
let fontPath = bundle.path(forResource: name, ofType: "otf")
|
||||
let fontDataProvider = CGDataProvider(filename: fontPath!)
|
||||
self.defaultCGFont = CGFont(fontDataProvider!)!
|
||||
//print("Num glyphs: \(self.defaultCGFont.numberOfGlyphs)")
|
||||
|
||||
self.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil);
|
||||
|
||||
//print("Loading associated .plist")
|
||||
let mathTablePlist = bundle.url(forResource:name, withExtension:"plist")
|
||||
self.rawMathTable = NSDictionary(contentsOf: mathTablePlist!)
|
||||
self.mathTable = MTFontMathTable(withFont:self, mathTable:rawMathTable!)
|
||||
}
|
||||
|
||||
static var fontBundle:Bundle {
|
||||
// Uses bundle for class so that this can be access by the unit tests.
|
||||
Bundle(url: Bundle.main.url(forResource: "mathFonts", withExtension: "bundle")!)!
|
||||
}
|
||||
|
||||
/** Returns a copy of this font but with a different size. */
|
||||
public func copy(withSize size: CGFloat) -> MTFont {
|
||||
let newFont = MTFont()
|
||||
newFont.defaultCGFont = self.defaultCGFont
|
||||
newFont.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil)
|
||||
newFont.rawMathTable = self.rawMathTable
|
||||
newFont.mathTable = MTFontMathTable(withFont: newFont, mathTable: newFont.rawMathTable!)
|
||||
return newFont
|
||||
}
|
||||
|
||||
func get(nameForGlyph glyph:CGGlyph) -> String {
|
||||
let name = defaultCGFont.name(for: glyph) as? String
|
||||
return name ?? ""
|
||||
}
|
||||
|
||||
func get(glyphWithName name:String) -> CGGlyph {
|
||||
defaultCGFont.getGlyphWithGlyphName(name: name as CFString)
|
||||
}
|
||||
|
||||
/** The size of this font in points. */
|
||||
public var fontSize:CGFloat { CTFontGetSize(self.ctFont) }
|
||||
|
||||
deinit {
|
||||
self.ctFont = nil
|
||||
self.defaultCGFont = nil
|
||||
}
|
||||
|
||||
}
|
||||
93
third-party/SwiftMath/Sources/MathRender/MTFontManager.swift
vendored
Executable file
93
third-party/SwiftMath/Sources/MathRender/MTFontManager.swift
vendored
Executable file
|
|
@ -0,0 +1,93 @@
|
|||
|
||||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class MTFontManager {
|
||||
|
||||
static public private(set) var manager: MTFontManager = {
|
||||
MTFontManager()
|
||||
}()
|
||||
|
||||
let kDefaultFontSize = CGFloat(20)
|
||||
|
||||
static var fontManager : MTFontManager {
|
||||
return manager
|
||||
}
|
||||
|
||||
public init() { }
|
||||
|
||||
@RWLocked
|
||||
var nameToFontMap = [String: MTFont]()
|
||||
|
||||
public func font(withName name:String, size:CGFloat) -> MTFont? {
|
||||
var f = self.nameToFontMap[name]
|
||||
if f == nil {
|
||||
f = MTFont(fontWithName: name, size: size)
|
||||
self.nameToFontMap[name] = f
|
||||
}
|
||||
|
||||
if f!.fontSize == size { return f }
|
||||
else { return f!.copy(withSize: size) }
|
||||
}
|
||||
|
||||
public func latinModernFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "latinmodern-math", size: size)
|
||||
}
|
||||
|
||||
public func kpMathLightFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "KpMath-Light", size: size)
|
||||
}
|
||||
|
||||
public func kpMathSansFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "KpMath-Sans", size: size)
|
||||
}
|
||||
|
||||
public func xitsFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "xits-math", size: size)
|
||||
}
|
||||
|
||||
public func termesFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "texgyretermes-math", size: size)
|
||||
}
|
||||
|
||||
public func asanaFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "Asana-Math", size: size)
|
||||
}
|
||||
|
||||
public func eulerFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "Euler-Math", size: size)
|
||||
}
|
||||
|
||||
public func firaRegularFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "FiraMath-Regular", size: size)
|
||||
}
|
||||
|
||||
public func notoSansRegularFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "NotoSansMath-Regular", size: size)
|
||||
}
|
||||
|
||||
public func libertinusRegularFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "LibertinusMath-Regular", size: size)
|
||||
}
|
||||
|
||||
public func garamondMathFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "Garamond-Math", size: size)
|
||||
}
|
||||
|
||||
public func leteSansFont(withSize size:CGFloat) -> MTFont? {
|
||||
MTFontManager.fontManager.font(withName: "LeteSansMath", size: size)
|
||||
}
|
||||
|
||||
public var defaultFont: MTFont? {
|
||||
MTFontManager.fontManager.latinModernFont(withSize: kDefaultFontSize)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
355
third-party/SwiftMath/Sources/MathRender/MTFontMathTable.swift
vendored
Executable file
355
third-party/SwiftMath/Sources/MathRender/MTFontMathTable.swift
vendored
Executable file
|
|
@ -0,0 +1,355 @@
|
|||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreText
|
||||
|
||||
struct GlyphPart {
|
||||
/// The glyph that represents this part
|
||||
var glyph: CGGlyph!
|
||||
|
||||
/// Full advance width/height for this part, in the direction of the extension in points.
|
||||
var fullAdvance: CGFloat = 0
|
||||
|
||||
/// Advance width/ height of the straight bar connector material at the beginning of the glyph in points.
|
||||
var startConnectorLength: CGFloat = 0
|
||||
|
||||
/// Advance width/ height of the straight bar connector material at the end of the glyph in points.
|
||||
var endConnectorLength: CGFloat = 0
|
||||
|
||||
/// If this part is an extender. If set, the part can be skipped or repeated.
|
||||
var isExtender: Bool = false
|
||||
}
|
||||
|
||||
/** This class represents the Math table of an open type font.
|
||||
|
||||
The math table is documented here: https://www.microsoft.com/typography/otspec/math.htm
|
||||
|
||||
How the constants in this class affect the display is documented here:
|
||||
http://www.tug.org/TUGboat/tb30-1/tb94vieth.pdf
|
||||
|
||||
Note: We don't parse the math table from the open type font. Rather we parse it
|
||||
in python and convert it to a .plist file which is easily consumed by this class.
|
||||
This approach is preferable to spending an inordinate amount of time figuring out
|
||||
how to parse the returned NSData object using the open type rules.
|
||||
|
||||
Remark: This class is not meant to be used outside of this library.
|
||||
*/
|
||||
class MTFontMathTable {
|
||||
|
||||
// The font for this math table.
|
||||
public private(set) weak var font:MTFont? // @property (nonatomic, readonly, weak) MTFont* font;
|
||||
|
||||
var _unitsPerEm: UInt
|
||||
var _fontSize: CGFloat
|
||||
var _mathTable: NSDictionary!
|
||||
|
||||
let kConstants = "constants"
|
||||
|
||||
/** MU unit in points */
|
||||
var muUnit:CGFloat { _fontSize/18 }
|
||||
|
||||
func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
|
||||
CGFloat(fontUnits) * _fontSize / CGFloat(_unitsPerEm)
|
||||
}
|
||||
|
||||
init(withFont font: MTFont?, mathTable:NSDictionary) {
|
||||
assert(font != nil, "font has nil value")
|
||||
assert(font!.ctFont != nil, "font.ctFont has nil value")
|
||||
self.font = font
|
||||
// do domething with font
|
||||
_unitsPerEm = UInt(CTFontGetUnitsPerEm(font!.ctFont))
|
||||
_fontSize = font!.fontSize;
|
||||
_mathTable = mathTable
|
||||
let version = _mathTable["version"] as! String
|
||||
if version != "1.3" {
|
||||
NSException(name: NSExceptionName.internalInconsistencyException, reason: "Invalid version of math table plist: \(version)").raise()
|
||||
}
|
||||
}
|
||||
|
||||
func constantFromTable(_ constName:String) -> CGFloat {
|
||||
let consts = _mathTable[kConstants] as! NSDictionary?
|
||||
let val = consts![constName] as! NSNumber?
|
||||
return fontUnitsToPt(val!.intValue)
|
||||
}
|
||||
|
||||
func percentFromTable(_ percentName:String) -> CGFloat {
|
||||
let consts = _mathTable[kConstants] as! NSDictionary?
|
||||
let val = consts![percentName] as! NSNumber?
|
||||
return CGFloat(val!.floatValue) / 100
|
||||
}
|
||||
|
||||
/// Math Font Metrics from the opentype specification
|
||||
// MARK: - Fractions
|
||||
var fractionNumeratorDisplayStyleShiftUp:CGFloat { constantFromTable("FractionNumeratorDisplayStyleShiftUp") } // \sigma_8 in TeX
|
||||
var fractionNumeratorShiftUp:CGFloat { constantFromTable("FractionNumeratorShiftUp") } // \sigma_9 in TeX
|
||||
var fractionDenominatorDisplayStyleShiftDown:CGFloat { constantFromTable("FractionDenominatorDisplayStyleShiftDown") } // \sigma_11 in TeX
|
||||
var fractionDenominatorShiftDown:CGFloat { constantFromTable("FractionDenominatorShiftDown") } // \sigma_12 in TeX
|
||||
var fractionNumeratorDisplayStyleGapMin:CGFloat { constantFromTable("FractionNumDisplayStyleGapMin") } // 3 * \xi_8 in TeX
|
||||
var fractionNumeratorGapMin:CGFloat { constantFromTable("FractionNumeratorGapMin") } // \xi_8 in TeX
|
||||
var fractionDenominatorDisplayStyleGapMin:CGFloat { constantFromTable("FractionDenomDisplayStyleGapMin") } // 3 * \xi_8 in TeX
|
||||
var fractionDenominatorGapMin:CGFloat { constantFromTable("FractionDenominatorGapMin") } // \xi_8 in TeX
|
||||
var fractionRuleThickness:CGFloat { constantFromTable("FractionRuleThickness") } // \xi_8 in TeX
|
||||
var skewedFractionHorizonalGap:CGFloat { constantFromTable("SkewedFractionHorizontalGap") } // \sigma_20 in TeX
|
||||
var skewedFractionVerticalGap:CGFloat { constantFromTable("SkewedFractionVerticalGap") } // \sigma_21 in TeX
|
||||
|
||||
// MARK: - Non-standard
|
||||
/// FractionDelimiterSize and FractionDelimiterDisplayStyleSize are not constants
|
||||
/// specified in the OpenType Math specification. Rather these are proposed LuaTeX extensions
|
||||
/// for the TeX parameters \sigma_20 (delim1) and \sigma_21 (delim2). Since these do not
|
||||
/// exist in the fonts that we have, we use the same approach as LuaTeX and use the fontSize
|
||||
/// to determine these values. The constants used match the metrics values of the original TeX fonts.
|
||||
/// Note: An alternative approach is to use DelimitedSubFormulaMinHeight for \sigma21 and use a factor
|
||||
/// of 2 to get \sigma 20 as proposed in Vieth paper.
|
||||
/// The XeTeX implementation sets \sigma21 = fontSize and \sigma20 = DelimitedSubFormulaMinHeight which
|
||||
/// will produce smaller delimiters.
|
||||
/// Of all the approaches we've implemented LuaTeX's approach since it mimics LaTeX most accurately.
|
||||
var fractionDelimiterSize: CGFloat { 1.01 * _fontSize }
|
||||
|
||||
/// Modified constant from 2.4 to 2.39 for better visual appearance.
|
||||
var fractionDelimiterDisplayStyleSize: CGFloat { 2.39 * _fontSize }
|
||||
|
||||
// MARK: - Stacks
|
||||
var stackTopDisplayStyleShiftUp:CGFloat { constantFromTable("StackTopDisplayStyleShiftUp") } // \sigma_8 in TeX
|
||||
var stackTopShiftUp:CGFloat { constantFromTable("StackTopShiftUp") } // \sigma_10 in TeX
|
||||
var stackDisplayStyleGapMin:CGFloat { constantFromTable("StackDisplayStyleGapMin") } // 7 \xi_8 in TeX
|
||||
var stackGapMin:CGFloat { constantFromTable("StackGapMin") } // 3 \xi_8 in TeX
|
||||
var stackBottomDisplayStyleShiftDown:CGFloat { constantFromTable("StackBottomDisplayStyleShiftDown") } // \sigma_11 in TeX
|
||||
var stackBottomShiftDown:CGFloat { constantFromTable("StackBottomShiftDown") } // \sigma_12 in TeX
|
||||
|
||||
var stretchStackBottomShiftDown:CGFloat { constantFromTable("StretchStackBottomShiftDown") }
|
||||
var stretchStackGapAboveMin:CGFloat { constantFromTable("StretchStackGapAboveMin") }
|
||||
var stretchStackGapBelowMin:CGFloat { constantFromTable("StretchStackGapBelowMin") }
|
||||
var stretchStackTopShiftUp:CGFloat { constantFromTable("StretchStackTopShiftUp") }
|
||||
|
||||
// MARK: - super/sub scripts
|
||||
|
||||
var superscriptShiftUp:CGFloat { constantFromTable("SuperscriptShiftUp") } // \sigma_13, \sigma_14 in TeX
|
||||
var superscriptShiftUpCramped:CGFloat { constantFromTable("SuperscriptShiftUpCramped") } // \sigma_15 in TeX
|
||||
var subscriptShiftDown:CGFloat { constantFromTable("SubscriptShiftDown") } // \sigma_16, \sigma_17 in TeX
|
||||
var superscriptBaselineDropMax:CGFloat { constantFromTable("SuperscriptBaselineDropMax") } // \sigma_18 in TeX
|
||||
var subscriptBaselineDropMin:CGFloat { constantFromTable("SubscriptBaselineDropMin") } // \sigma_19 in TeX
|
||||
var superscriptBottomMin:CGFloat { constantFromTable("SuperscriptBottomMin") } // 1/4 \sigma_5 in TeX
|
||||
var subscriptTopMax:CGFloat { constantFromTable("SubscriptTopMax") } // 4/5 \sigma_5 in TeX
|
||||
var subSuperscriptGapMin:CGFloat { constantFromTable("SubSuperscriptGapMin") } // 4 \xi_8 in TeX
|
||||
var superscriptBottomMaxWithSubscript:CGFloat { constantFromTable("SuperscriptBottomMaxWithSubscript") } // 4/5 \sigma_5 in TeX
|
||||
|
||||
var spaceAfterScript:CGFloat { constantFromTable("SpaceAfterScript") }
|
||||
|
||||
// MARK: - radicals
|
||||
var radicalExtraAscender:CGFloat { constantFromTable("RadicalExtraAscender") } // \xi_8 in Tex
|
||||
var radicalRuleThickness:CGFloat { constantFromTable("RadicalRuleThickness") } // \xi_8 in Tex
|
||||
var radicalDisplayStyleVerticalGap:CGFloat { constantFromTable("RadicalDisplayStyleVerticalGap") } // \xi_8 + 1/4 \sigma_5 in Tex
|
||||
var radicalVerticalGap:CGFloat { constantFromTable("RadicalVerticalGap") } // 5/4 \xi_8 in Tex
|
||||
var radicalKernBeforeDegree:CGFloat { constantFromTable("RadicalKernBeforeDegree") } // 5 mu in Tex
|
||||
var radicalKernAfterDegree:CGFloat { constantFromTable("RadicalKernAfterDegree") } // -10 mu in Tex
|
||||
var radicalDegreeBottomRaisePercent:CGFloat { percentFromTable("RadicalDegreeBottomRaisePercent") } // 60% in Tex
|
||||
|
||||
// MARK: - Limits
|
||||
var upperLimitBaselineRiseMin:CGFloat { constantFromTable("UpperLimitBaselineRiseMin") } // \xi_11 in TeX
|
||||
var upperLimitGapMin:CGFloat { constantFromTable("UpperLimitGapMin") } // \xi_9 in TeX
|
||||
var lowerLimitGapMin:CGFloat { constantFromTable("LowerLimitGapMin") } // \xi_10 in TeX
|
||||
var lowerLimitBaselineDropMin:CGFloat { constantFromTable("LowerLimitBaselineDropMin") } // \xi_12 in TeX
|
||||
var limitExtraAscenderDescender:CGFloat { 0 } // \xi_13 in TeX, not present in OpenType so we always set it to 0.
|
||||
|
||||
// MARK: - Underline
|
||||
var underbarVerticalGap:CGFloat { constantFromTable("UnderbarVerticalGap") } // 3 \xi_8 in TeX
|
||||
var underbarRuleThickness:CGFloat { constantFromTable("UnderbarRuleThickness") } // \xi_8 in TeX
|
||||
var underbarExtraDescender:CGFloat { constantFromTable("UnderbarExtraDescender") } // \xi_8 in TeX
|
||||
|
||||
// MARK: - Overline
|
||||
var overbarVerticalGap:CGFloat { constantFromTable("OverbarVerticalGap") } // 3 \xi_8 in TeX
|
||||
var overbarRuleThickness:CGFloat { constantFromTable("OverbarRuleThickness") } // \xi_8 in TeX
|
||||
var overbarExtraAscender:CGFloat { constantFromTable("OverbarExtraAscender") } // \xi_8 in TeX
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
var axisHeight:CGFloat { constantFromTable("AxisHeight") } // \sigma_22 in TeX
|
||||
var scriptScaleDown:CGFloat { percentFromTable("ScriptPercentScaleDown") }
|
||||
var scriptScriptScaleDown:CGFloat { percentFromTable("ScriptScriptPercentScaleDown") }
|
||||
var mathLeading:CGFloat { constantFromTable("MathLeading") }
|
||||
var delimitedSubFormulaMinHeight:CGFloat { constantFromTable("DelimitedSubFormulaMinHeight") }
|
||||
|
||||
// MARK: - Accent
|
||||
|
||||
var accentBaseHeight:CGFloat { constantFromTable("AccentBaseHeight") } // \fontdimen5 in TeX (x-height)
|
||||
var flattenedAccentBaseHeight:CGFloat { constantFromTable("FlattenedAccentBaseHeight") }
|
||||
|
||||
// MARK: - Variants
|
||||
|
||||
let kVertVariants = "v_variants"
|
||||
let kHorizVariants = "h_variants"
|
||||
|
||||
/** Returns an Array of all the vertical variants of the glyph if any. If
|
||||
there are no variants for the glyph, the array contains the given glyph. */
|
||||
func getVerticalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
|
||||
let variants = _mathTable[kVertVariants] as! NSDictionary?
|
||||
return self.getVariantsForGlyph(glyph, inDictionary: variants!)
|
||||
}
|
||||
|
||||
/** Returns an Array of all the horizontal variants of the glyph if any. If
|
||||
there are no variants for the glyph, the array contains the given glyph. */
|
||||
func getHorizontalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
|
||||
let variants = _mathTable[kHorizVariants] as! NSDictionary
|
||||
return self.getVariantsForGlyph(glyph, inDictionary:variants)
|
||||
}
|
||||
|
||||
func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants:NSDictionary) -> [NSNumber?] {
|
||||
let glyphName = self.font!.get(nameForGlyph: glyph)
|
||||
let variantGlyphs = variants[glyphName] as! NSArray?
|
||||
var glyphArray = [NSNumber]()
|
||||
if variantGlyphs == nil || variantGlyphs?.count == 0 {
|
||||
// There are no extra variants, so just add the current glyph to it.
|
||||
let glyph = self.font!.get(glyphWithName: glyphName)
|
||||
glyphArray.append(NSNumber(value:glyph))
|
||||
return glyphArray
|
||||
}
|
||||
for gvn in variantGlyphs! {
|
||||
let glyphVariantName = gvn as! String?
|
||||
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
|
||||
glyphArray.append(NSNumber(value:variantGlyph!))
|
||||
}
|
||||
return glyphArray
|
||||
}
|
||||
|
||||
/** Returns a larger vertical variant of the given glyph if any.
|
||||
If there is no larger version, this returns the current glyph.
|
||||
|
||||
- Parameter glyph: The glyph to find a larger variant for
|
||||
- Parameter forDisplayStyle: If true, selects the largest appropriate variant for display style.
|
||||
If false, selects the next larger variant (incremental sizing).
|
||||
- Returns: A larger glyph variant, or the original glyph if no variants exist
|
||||
*/
|
||||
func getLargerGlyph(_ glyph:CGGlyph, forDisplayStyle: Bool = false) -> CGGlyph {
|
||||
let variants = _mathTable[kVertVariants] as! NSDictionary?
|
||||
let glyphName = self.font?.get(nameForGlyph: glyph)
|
||||
let variantGlyphs = variants![glyphName!] as! NSArray?
|
||||
if variantGlyphs == nil || variantGlyphs?.count == 0 {
|
||||
// There are no extra variants, so just returnt the current glyph.
|
||||
return glyph
|
||||
}
|
||||
|
||||
if forDisplayStyle {
|
||||
// For display style, select a large variant suitable for mathematical display mode
|
||||
// Display integrals should be significantly larger (~2.2em) for visual prominence
|
||||
let count = variantGlyphs!.count
|
||||
|
||||
// Strategy: Use the largest variant, but avoid extreme sizes for fonts with many variants
|
||||
let targetIndex: Int
|
||||
if count <= 2 {
|
||||
// Small variant list: use the last one (e.g., integral.size1 at ~2.2em)
|
||||
targetIndex = count - 1
|
||||
} else if count <= 4 {
|
||||
// Medium variant list: use second-to-last to avoid extremes
|
||||
targetIndex = count - 2
|
||||
} else {
|
||||
// Large variant list (like texgyretermes with 6 variants):
|
||||
// Use variant at ~60% position to get appropriate display size (~2.0em)
|
||||
// For 7 variants (0-6), this gives index 4
|
||||
targetIndex = min(count - 2, Int(Double(count) * 0.6))
|
||||
}
|
||||
|
||||
if let glyphVariantName = variantGlyphs![targetIndex] as? String {
|
||||
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName)
|
||||
return variantGlyph!
|
||||
}
|
||||
} else {
|
||||
// Text/inline style: use incremental sizing for moderate enlargement
|
||||
// Find the first variant with a different name
|
||||
for gvn in variantGlyphs! {
|
||||
let glyphVariantName = gvn as! String?
|
||||
if glyphVariantName != glyphName {
|
||||
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
|
||||
return variantGlyph!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We did not find any variants of this glyph so return it.
|
||||
return glyph;
|
||||
}
|
||||
|
||||
// MARK: - Italic Correction
|
||||
|
||||
let kItalic = "italic"
|
||||
|
||||
/** Returns the italic correction for the given glyph if any. If there
|
||||
isn't any this returns 0. */
|
||||
func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat {
|
||||
let italics = _mathTable[kItalic] as! NSDictionary?
|
||||
let glyphName = self.font?.get(nameForGlyph: glyph)
|
||||
let val = italics![glyphName!] as! NSNumber?
|
||||
// if val is nil, this returns 0.
|
||||
return self.fontUnitsToPt(val?.intValue ?? 0)
|
||||
}
|
||||
|
||||
// MARK: - Accents
|
||||
|
||||
let kAccents = "accents"
|
||||
|
||||
/** Returns the adjustment to the top accent for the given glyph if any.
|
||||
If there isn't any this returns -1. */
|
||||
func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat {
|
||||
var glyph = glyph
|
||||
let accents = _mathTable[kAccents] as! NSDictionary?
|
||||
let glyphName = self.font?.get(nameForGlyph: glyph)
|
||||
let val = accents![glyphName!] as! NSNumber?
|
||||
if let val = val {
|
||||
return self.fontUnitsToPt(val.intValue)
|
||||
} else {
|
||||
// If no top accent is defined then it is the center of the advance width.
|
||||
var advances = CGSize.zero
|
||||
CTFontGetAdvancesForGlyphs(self.font!.ctFont, .horizontal, &glyph, &advances, 1)
|
||||
return advances.width/2
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Glyph Construction
|
||||
|
||||
/** Minimum overlap of connecting glyphs during glyph construction */
|
||||
var minConnectorOverlap:CGFloat { constantFromTable("MinConnectorOverlap") }
|
||||
|
||||
let kVertAssembly = "v_assembly"
|
||||
let kAssemblyParts = "parts"
|
||||
|
||||
/** Returns an array of the glyph parts to be used for constructing vertical variants
|
||||
of this glyph. If there is no glyph assembly defined, returns an empty array. */
|
||||
func getVerticalGlyphAssembly(forGlyph glyph:CGGlyph) -> [GlyphPart] {
|
||||
let assemblyTable = _mathTable[kVertAssembly] as! NSDictionary?
|
||||
let glyphName = self.font?.get(nameForGlyph: glyph)
|
||||
let assemblyInfo = assemblyTable![glyphName!] as! NSDictionary?
|
||||
if assemblyInfo == nil {
|
||||
// No vertical assembly defined for glyph
|
||||
return []
|
||||
}
|
||||
let parts = assemblyInfo![kAssemblyParts] as! NSArray?
|
||||
if parts == nil {
|
||||
// parts should always have been defined, but if it isn't return nil
|
||||
return []
|
||||
}
|
||||
var rv = [GlyphPart]()
|
||||
for part in parts! {
|
||||
let partInfo = part as! NSDictionary?
|
||||
var part = GlyphPart()
|
||||
let adv = partInfo!["advance"] as! NSNumber?
|
||||
part.fullAdvance = self.fontUnitsToPt(adv!.intValue)
|
||||
let end = partInfo!["endConnector"] as! NSNumber?
|
||||
part.endConnectorLength = self.fontUnitsToPt(end!.intValue)
|
||||
let start = partInfo!["startConnector"] as! NSNumber?
|
||||
part.startConnectorLength = self.fontUnitsToPt(start!.intValue)
|
||||
let ext = partInfo!["extender"] as! NSNumber?
|
||||
part.isExtender = ext!.boolValue
|
||||
let glyphName = partInfo!["glyph"] as! String?
|
||||
part.glyph = self.font?.get(glyphWithName: glyphName!)
|
||||
rv.append(part)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
37
third-party/SwiftMath/Sources/MathRender/MTLabel.swift
vendored
Executable file
37
third-party/SwiftMath/Sources/MathRender/MTLabel.swift
vendored
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by 安志钢.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
public class MTLabel : NSTextField {
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
self.stringValue = ""
|
||||
self.isBezeled = false
|
||||
self.drawsBackground = false
|
||||
self.isEditable = false
|
||||
self.isSelectable = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
// MARK: - Customized getter and setter methods for property text.
|
||||
var text:String? {
|
||||
get { super.stringValue }
|
||||
set { super.stringValue = newValue! }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
1141
third-party/SwiftMath/Sources/MathRender/MTMathAtomFactory.swift
vendored
Normal file
1141
third-party/SwiftMath/Sources/MathRender/MTMathAtomFactory.swift
vendored
Normal file
File diff suppressed because it is too large
Load diff
117
third-party/SwiftMath/Sources/MathRender/MTMathImage.swift
vendored
Executable file
117
third-party/SwiftMath/Sources/MathRender/MTMathImage.swift
vendored
Executable file
|
|
@ -0,0 +1,117 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Peter Tang on 12/9/2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public class MTMathImage {
|
||||
public var font: MTFont? = MTFontManager.fontManager.defaultFont
|
||||
public var fontSize:CGFloat {
|
||||
set {
|
||||
_fontSize = newValue
|
||||
let font = font?.copy(withSize: newValue)
|
||||
self.font = font // also forces an update
|
||||
}
|
||||
get { _fontSize }
|
||||
}
|
||||
private var _fontSize:CGFloat = 0
|
||||
public let textColor: MTColor
|
||||
|
||||
public let labelMode: MTMathUILabelMode
|
||||
public let textAlignment: MTTextAlignment
|
||||
|
||||
public var contentInsets: MTEdgeInsets = MTEdgeInsetsZero
|
||||
|
||||
public let latex: String
|
||||
private(set) var intrinsicContentSize = CGSize.zero
|
||||
|
||||
public init(latex: String, fontSize: CGFloat, textColor: MTColor, labelMode: MTMathUILabelMode = .display, textAlignment: MTTextAlignment = .center) {
|
||||
self.latex = latex
|
||||
self.textColor = textColor
|
||||
self.labelMode = labelMode
|
||||
self.textAlignment = textAlignment
|
||||
self.fontSize = fontSize
|
||||
}
|
||||
}
|
||||
extension MTMathImage {
|
||||
public var currentStyle: MTLineStyle {
|
||||
switch labelMode {
|
||||
case .display: return .display
|
||||
case .text: return .text
|
||||
}
|
||||
}
|
||||
private func intrinsicContentSize(_ displayList: MTMathListDisplay) -> CGSize {
|
||||
CGSize(width: displayList.width + contentInsets.left + contentInsets.right,
|
||||
height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom)
|
||||
}
|
||||
public func asImage() -> (NSError?, MTImage?) {
|
||||
func layoutImage(size: CGSize, displayList: MTMathListDisplay) {
|
||||
var textX = CGFloat(0)
|
||||
switch self.textAlignment {
|
||||
case .left: textX = contentInsets.left
|
||||
case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
|
||||
case .right: textX = size.width - displayList.width - contentInsets.right
|
||||
}
|
||||
let availableHeight = size.height - contentInsets.bottom - contentInsets.top
|
||||
|
||||
// center things vertically
|
||||
var height = displayList.ascent + displayList.descent
|
||||
if height < fontSize/2 {
|
||||
height = fontSize/2 // set height to half the font size
|
||||
}
|
||||
let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom
|
||||
displayList.position = CGPoint(x: textX, y: textY)
|
||||
}
|
||||
|
||||
var error: NSError?
|
||||
guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil,
|
||||
let displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle) else {
|
||||
return (error, nil)
|
||||
}
|
||||
|
||||
intrinsicContentSize = intrinsicContentSize(displayList)
|
||||
displayList.textColor = textColor
|
||||
|
||||
let size = intrinsicContentSize
|
||||
layoutImage(size: size, displayList: displayList)
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
let image = renderer.image { rendererContext in
|
||||
rendererContext.cgContext.saveGState()
|
||||
rendererContext.cgContext.concatenate(.flippedVertically(size.height))
|
||||
displayList.draw(rendererContext.cgContext)
|
||||
rendererContext.cgContext.restoreGState()
|
||||
}
|
||||
return (nil, image)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
let image = NSImage(size: size, flipped: false) { bounds in
|
||||
guard let context = NSGraphicsContext.current?.cgContext else { return false }
|
||||
context.saveGState()
|
||||
displayList.draw(context)
|
||||
context.restoreGState()
|
||||
return true
|
||||
}
|
||||
return (nil, image)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
private extension CGAffineTransform {
|
||||
static func flippedVertically(_ height: CGFloat) -> CGAffineTransform {
|
||||
var transform = CGAffineTransform(scaleX: 1, y: -1)
|
||||
transform = transform.translatedBy(x: 0, y: -height)
|
||||
return transform
|
||||
}
|
||||
}
|
||||
1058
third-party/SwiftMath/Sources/MathRender/MTMathList.swift
vendored
Normal file
1058
third-party/SwiftMath/Sources/MathRender/MTMathList.swift
vendored
Normal file
File diff suppressed because it is too large
Load diff
1613
third-party/SwiftMath/Sources/MathRender/MTMathListBuilder.swift
vendored
Normal file
1613
third-party/SwiftMath/Sources/MathRender/MTMathListBuilder.swift
vendored
Normal file
File diff suppressed because it is too large
Load diff
881
third-party/SwiftMath/Sources/MathRender/MTMathListDisplay.swift
vendored
Executable file
881
third-party/SwiftMath/Sources/MathRender/MTMathListDisplay.swift
vendored
Executable file
|
|
@ -0,0 +1,881 @@
|
|||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import QuartzCore
|
||||
import CoreText
|
||||
import SwiftUI
|
||||
|
||||
func isIos6Supported() -> Bool {
|
||||
if !MTDisplay.initialized {
|
||||
#if os(iOS) || os(visionOS)
|
||||
let reqSysVer = "6.0"
|
||||
let currSysVer = UIDevice.current.systemVersion
|
||||
if currSysVer.compare(reqSysVer, options: .numeric) != .orderedAscending {
|
||||
MTDisplay.supported = true
|
||||
}
|
||||
#else
|
||||
MTDisplay.supported = true
|
||||
#endif
|
||||
MTDisplay.initialized = true
|
||||
}
|
||||
return MTDisplay.supported
|
||||
}
|
||||
|
||||
// The Downshift protocol allows an MTDisplay to be shifted down by a given amount.
|
||||
protocol DownShift {
|
||||
var shiftDown:CGFloat { set get }
|
||||
}
|
||||
|
||||
// MARK: - MTDisplay
|
||||
|
||||
/// The base class for rendering a math equation.
|
||||
public class MTDisplay:NSObject {
|
||||
|
||||
// needed for isIos6Supported() func above
|
||||
static var initialized = false
|
||||
static var supported = false
|
||||
|
||||
/// Draws itself in the given graphics context.
|
||||
public func draw(_ context:CGContext) {
|
||||
if self.localBackgroundColor != nil {
|
||||
context.saveGState()
|
||||
context.setBlendMode(.normal)
|
||||
context.setFillColor(self.localBackgroundColor!.cgColor)
|
||||
context.fill(self.displayBounds())
|
||||
context.restoreGState()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the bounding rectangle for the MTDisplay
|
||||
func displayBounds() -> CGRect {
|
||||
CGRectMake(self.position.x, self.position.y - self.descent, self.width, self.ascent + self.descent)
|
||||
}
|
||||
|
||||
/// For debugging. Shows the object in quick look in Xcode.
|
||||
#if os(iOS) || os(visionOS)
|
||||
func debugQuickLookObject() -> Any {
|
||||
let size = CGSizeMake(self.width, self.ascent + self.descent);
|
||||
UIGraphicsBeginImageContext(size);
|
||||
|
||||
// get a reference to that context we created
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
// translate/flip the graphics context (for transforming from CG* coords to UI* coords
|
||||
context.translateBy(x: 0, y: size.height);
|
||||
context.scaleBy(x: 1.0, y: -1.0);
|
||||
// move the position to (0,0)
|
||||
context.translateBy(x: -self.position.x, y: -self.position.y);
|
||||
|
||||
// Move the line up by self.descent
|
||||
context.translateBy(x: 0, y: self.descent);
|
||||
// Draw self on context
|
||||
self.draw(context)
|
||||
|
||||
// generate a new UIImage from the graphics context we drew onto
|
||||
let img = UIGraphicsGetImageFromCurrentImageContext()
|
||||
return img as Any
|
||||
}
|
||||
#endif
|
||||
|
||||
/// The distance from the axis to the top of the display
|
||||
public var ascent:CGFloat = 0
|
||||
/// The distance from the axis to the bottom of the display
|
||||
public var descent:CGFloat = 0
|
||||
/// The width of the display
|
||||
public var width:CGFloat = 0
|
||||
/// Position of the display with respect to the parent view or display.
|
||||
var position = CGPoint.zero
|
||||
/// The range of characters supported by this item
|
||||
public var range:NSRange=NSMakeRange(0, 0)
|
||||
/// Whether the display has a subscript/superscript following it.
|
||||
public var hasScript:Bool = false
|
||||
/// The text color for this display
|
||||
var textColor: MTColor?
|
||||
/// The local color, if the color was mutated local with the color command
|
||||
var localTextColor: MTColor?
|
||||
/// The background color for this display
|
||||
var localBackgroundColor: MTColor?
|
||||
|
||||
}
|
||||
|
||||
/// Special class to be inherited from that implements the DownShift protocol
|
||||
class MTDisplayDS : MTDisplay, DownShift {
|
||||
|
||||
var shiftDown: CGFloat = 0
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTCTLineDisplay
|
||||
|
||||
/// A rendering of a single CTLine as an MTDisplay
|
||||
public class MTCTLineDisplay : MTDisplay {
|
||||
|
||||
/// The CTLine being displayed
|
||||
public var line:CTLine!
|
||||
/// The attributed string used to generate the CTLineRef. Note setting this does not reset the dimensions of
|
||||
/// the display. So set only when
|
||||
var attributedString:NSAttributedString? {
|
||||
didSet {
|
||||
line = CTLineCreateWithAttributedString(attributedString!)
|
||||
}
|
||||
}
|
||||
|
||||
/// An array of MTMathAtoms that this CTLine displays. Used for indexing back into the MTMathList
|
||||
public fileprivate(set) var atoms = [MTMathAtom]()
|
||||
|
||||
init(withString attrString:NSAttributedString?, position:CGPoint, range:NSRange, font:MTFont?, atoms:[MTMathAtom]) {
|
||||
super.init()
|
||||
self.position = position
|
||||
self.attributedString = attrString
|
||||
self.line = CTLineCreateWithAttributedString(attrString!)
|
||||
self.range = range
|
||||
self.atoms = atoms
|
||||
// We can't use typographic bounds here as the ascent and descent returned are for the font and not for the line.
|
||||
// CRITICAL FIX for accented character clipping:
|
||||
// Use the MAXIMUM of typographic width and visual width to account for glyph overhang.
|
||||
// - Typographic width = advance width (how far the cursor moves)
|
||||
// - Visual width = actual glyph extent (CGRectGetMaxX of glyph path bounds)
|
||||
// Some glyphs (especially italic/oblique accented characters) extend beyond their advance width.
|
||||
// Using max() ensures we account for overhang while maintaining proper spacing for normal glyphs.
|
||||
let typographicWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil))
|
||||
if isIos6Supported() {
|
||||
let bounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
|
||||
self.ascent = max(0, CGRectGetMaxY(bounds) - 0);
|
||||
self.descent = max(0, 0 - CGRectGetMinY(bounds));
|
||||
// Use the maximum of visual and typographic width to handle both:
|
||||
// 1. Overhanging glyphs (visual > typographic) - prevents clipping
|
||||
// 2. Normal glyphs (typographic >= visual) - maintains correct spacing
|
||||
let visualWidth = CGRectGetMaxX(bounds)
|
||||
self.width = max(typographicWidth, visualWidth);
|
||||
} else {
|
||||
// Our own implementation of the ios6 function to get glyph path bounds.
|
||||
self.computeDimensions(font, typographicWidth: typographicWidth)
|
||||
}
|
||||
}
|
||||
|
||||
override var textColor: MTColor? {
|
||||
set {
|
||||
super.textColor = newValue
|
||||
let attrStr = attributedString!.mutableCopy() as! NSMutableAttributedString
|
||||
let foregroundColor = NSAttributedString.Key(kCTForegroundColorAttributeName as String)
|
||||
attrStr.addAttribute(foregroundColor, value:self.textColor!.cgColor, range:NSMakeRange(0, attrStr.length))
|
||||
self.attributedString = attrStr
|
||||
}
|
||||
get { super.textColor }
|
||||
}
|
||||
|
||||
func computeDimensions(_ font:MTFont?, typographicWidth: CGFloat) {
|
||||
let runs = CTLineGetGlyphRuns(line) as NSArray
|
||||
var maxVisualWidth: CGFloat = 0
|
||||
var currentX: CGFloat = 0
|
||||
|
||||
for obj in runs {
|
||||
let run = obj as! CTRun?
|
||||
let numGlyphs = CTRunGetGlyphCount(run!)
|
||||
var glyphs = [CGGlyph]()
|
||||
glyphs.reserveCapacity(numGlyphs)
|
||||
CTRunGetGlyphs(run!, CFRangeMake(0, numGlyphs), &glyphs);
|
||||
let bounds = CTFontGetBoundingRectsForGlyphs(font!.ctFont, .horizontal, glyphs, nil, numGlyphs);
|
||||
let ascent = max(0, CGRectGetMaxY(bounds) - 0);
|
||||
// Descent is how much the line goes below the origin. However if the line is all above the origin, then descent can't be negative.
|
||||
let descent = max(0, 0 - CGRectGetMinY(bounds));
|
||||
if (ascent > self.ascent) {
|
||||
self.ascent = ascent;
|
||||
}
|
||||
if (descent > self.descent) {
|
||||
self.descent = descent;
|
||||
}
|
||||
|
||||
// Calculate visual width using glyph extent
|
||||
// Get the rightmost edge of this run's glyphs
|
||||
let runVisualWidth = CGRectGetMaxX(bounds)
|
||||
let runRightEdge = currentX + runVisualWidth
|
||||
|
||||
// Get advances to know where next run starts
|
||||
var advances = [CGSize](repeating: CGSize.zero, count: numGlyphs)
|
||||
CTRunGetAdvances(run!, CFRangeMake(0, numGlyphs), &advances)
|
||||
for advance in advances {
|
||||
currentX += advance.width
|
||||
}
|
||||
|
||||
if (runRightEdge > maxVisualWidth) {
|
||||
maxVisualWidth = runRightEdge
|
||||
}
|
||||
}
|
||||
|
||||
// Use maximum of typographic and visual width
|
||||
self.width = max(typographicWidth, maxVisualWidth)
|
||||
}
|
||||
|
||||
override public func draw(_ context: CGContext) {
|
||||
super.draw(context)
|
||||
context.saveGState()
|
||||
|
||||
context.textPosition = self.position
|
||||
CTLineDraw(line, context)
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTMathListDisplay
|
||||
|
||||
/// An MTLine is a rendered form of MTMathList in one line.
|
||||
/// It can render itself using the draw method.
|
||||
public class MTMathListDisplay : MTDisplay {
|
||||
|
||||
/**
|
||||
The type of position for a line, i.e. subscript/superscript or regular.
|
||||
*/
|
||||
public enum LinePosition : Int {
|
||||
/// Regular
|
||||
case regular
|
||||
/// Positioned at a subscript
|
||||
case ssubscript
|
||||
/// Positioned at a superscript
|
||||
case superscript
|
||||
}
|
||||
|
||||
/// Where the line is positioned
|
||||
public var type:LinePosition = .regular
|
||||
/// An array of MTDisplays which are positioned relative to the position of the
|
||||
/// the current display.
|
||||
public fileprivate(set) var subDisplays = [MTDisplay]()
|
||||
/// If a subscript or superscript this denotes the location in the parent MTList. For a
|
||||
/// regular list this is NSNotFound
|
||||
public var index: Int = 0
|
||||
|
||||
init(withDisplays displays:[MTDisplay], range:NSRange) {
|
||||
super.init()
|
||||
self.subDisplays = displays
|
||||
self.position = CGPoint.zero
|
||||
self.type = .regular
|
||||
self.index = NSNotFound
|
||||
self.range = range
|
||||
self.recomputeDimensions()
|
||||
}
|
||||
|
||||
override var textColor: MTColor? {
|
||||
set {
|
||||
super.textColor = newValue
|
||||
for displayAtom in self.subDisplays {
|
||||
if displayAtom.localTextColor == nil {
|
||||
displayAtom.textColor = newValue
|
||||
} else {
|
||||
displayAtom.textColor = displayAtom.localTextColor
|
||||
}
|
||||
}
|
||||
}
|
||||
get { super.textColor }
|
||||
}
|
||||
|
||||
override public func draw(_ context: CGContext) {
|
||||
super.draw(context)
|
||||
context.saveGState()
|
||||
|
||||
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
||||
context.translateBy(x: self.position.x, y: self.position.y)
|
||||
context.textPosition = CGPoint.zero
|
||||
|
||||
// draw each atom separately
|
||||
for displayAtom in self.subDisplays {
|
||||
displayAtom.draw(context)
|
||||
}
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
func recomputeDimensions() {
|
||||
var max_ascent:CGFloat = 0
|
||||
var max_descent:CGFloat = 0
|
||||
var max_width:CGFloat = 0
|
||||
for atom in self.subDisplays {
|
||||
let ascent = max(0, atom.position.y + atom.ascent);
|
||||
if (ascent > max_ascent) {
|
||||
max_ascent = ascent;
|
||||
}
|
||||
|
||||
let descent = max(0, 0 - (atom.position.y - atom.descent));
|
||||
if (descent > max_descent) {
|
||||
max_descent = descent;
|
||||
}
|
||||
let width = atom.width + atom.position.x;
|
||||
if (width > max_width) {
|
||||
max_width = width;
|
||||
}
|
||||
}
|
||||
self.ascent = max_ascent;
|
||||
self.descent = max_descent;
|
||||
self.width = max_width;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTFractionDisplay
|
||||
|
||||
/// Rendering of an MTFraction as an MTDisplay
|
||||
public class MTFractionDisplay : MTDisplay {
|
||||
|
||||
/** A display representing the numerator of the fraction. Its position is relative
|
||||
to the parent and is not treated as a sub-display.
|
||||
*/
|
||||
public fileprivate(set) var numerator:MTMathListDisplay?
|
||||
/** A display representing the denominator of the fraction. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
public fileprivate(set) var denominator:MTMathListDisplay?
|
||||
|
||||
var numeratorUp:CGFloat=0 { didSet { self.updateNumeratorPosition() } }
|
||||
var denominatorDown:CGFloat=0 { didSet { self.updateDenominatorPosition() } }
|
||||
var linePosition:CGFloat=0
|
||||
var lineThickness:CGFloat=0
|
||||
|
||||
init(withNumerator numerator:MTMathListDisplay?, denominator:MTMathListDisplay?, position:CGPoint, range:NSRange) {
|
||||
super.init()
|
||||
self.numerator = numerator;
|
||||
self.denominator = denominator;
|
||||
self.position = position;
|
||||
|
||||
// CRITICAL FIX: Handle invalid ranges gracefully
|
||||
// When table cells are typeset independently with maxWidth, atoms may have
|
||||
// ranges that are invalid in the cell's context (e.g., (0,0) or other issues)
|
||||
// Instead of crashing with assertion, normalize the range
|
||||
if range.length == 0 {
|
||||
// Create a dummy range with length 1 at the given location
|
||||
self.range = NSMakeRange(range.location, 1)
|
||||
} else {
|
||||
self.range = range;
|
||||
// Still assert for debugging if range length is something unexpected
|
||||
assert(self.range.length == 1, "Fraction range length not 1 - range (\(range.location), \(range.length)")
|
||||
}
|
||||
}
|
||||
|
||||
override public var ascent:CGFloat {
|
||||
set { super.ascent = newValue }
|
||||
get { (numerator?.ascent ?? 0) + self.numeratorUp }
|
||||
}
|
||||
|
||||
override public var descent:CGFloat {
|
||||
set { super.descent = newValue }
|
||||
get { (denominator?.descent ?? 0) + self.denominatorDown }
|
||||
}
|
||||
|
||||
override public var width:CGFloat {
|
||||
set { super.width = newValue }
|
||||
get { max(numerator?.width ?? 0, denominator?.width ?? 0) }
|
||||
}
|
||||
|
||||
func updateDenominatorPosition() {
|
||||
guard denominator != nil else { return }
|
||||
denominator!.position = CGPointMake(self.position.x + (self.width - denominator!.width)/2, self.position.y - self.denominatorDown)
|
||||
}
|
||||
|
||||
func updateNumeratorPosition() {
|
||||
guard numerator != nil else { return }
|
||||
numerator!.position = CGPointMake(self.position.x + (self.width - numerator!.width)/2, self.position.y + self.numeratorUp)
|
||||
}
|
||||
|
||||
override var position: CGPoint {
|
||||
set {
|
||||
super.position = newValue
|
||||
self.updateDenominatorPosition()
|
||||
self.updateNumeratorPosition()
|
||||
}
|
||||
get { super.position }
|
||||
}
|
||||
|
||||
override var textColor: MTColor? {
|
||||
set {
|
||||
super.textColor = newValue
|
||||
numerator?.textColor = newValue
|
||||
denominator?.textColor = newValue
|
||||
}
|
||||
get { super.textColor }
|
||||
}
|
||||
|
||||
override public func draw(_ context:CGContext) {
|
||||
super.draw(context)
|
||||
numerator?.draw(context)
|
||||
denominator?.draw(context)
|
||||
|
||||
context.saveGState()
|
||||
|
||||
self.textColor?.setStroke()
|
||||
|
||||
// draw the horizontal line
|
||||
// Note: line thickness of 0 draws the thinnest possible line - we want no line so check for 0s
|
||||
if self.lineThickness > 0 {
|
||||
let path = MTBezierPath()
|
||||
path.move(to: CGPointMake(self.position.x, self.position.y + self.linePosition))
|
||||
path.addLine(to: CGPointMake(self.position.x + self.width, self.position.y + self.linePosition))
|
||||
path.lineWidth = self.lineThickness
|
||||
path.stroke()
|
||||
}
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTRadicalDisplay
|
||||
|
||||
/// Rendering of an MTRadical as an MTDisplay
|
||||
class MTRadicalDisplay : MTDisplay {
|
||||
|
||||
/** A display representing the radicand of the radical. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
public fileprivate(set) var radicand:MTMathListDisplay?
|
||||
/** A display representing the degree of the radical. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
public fileprivate(set) var degree:MTMathListDisplay?
|
||||
|
||||
override var position: CGPoint {
|
||||
set {
|
||||
super.position = newValue
|
||||
self.updateRadicandPosition()
|
||||
}
|
||||
get { super.position }
|
||||
}
|
||||
|
||||
override var textColor: MTColor? {
|
||||
set {
|
||||
super.textColor = newValue
|
||||
self.radicand?.textColor = newValue
|
||||
self.degree?.textColor = newValue
|
||||
}
|
||||
get { super.textColor }
|
||||
}
|
||||
|
||||
private var _radicalGlyph:MTDisplay?
|
||||
private var _radicalShift:CGFloat=0
|
||||
|
||||
var topKern:CGFloat=0
|
||||
var lineThickness:CGFloat=0
|
||||
|
||||
init(withRadicand radicand:MTMathListDisplay?, glyph:MTDisplay, position:CGPoint, range:NSRange) {
|
||||
super.init()
|
||||
self.radicand = radicand
|
||||
_radicalGlyph = glyph
|
||||
_radicalShift = 0
|
||||
|
||||
self.position = position
|
||||
self.range = range
|
||||
}
|
||||
|
||||
func setDegree(_ degree:MTMathListDisplay?, fontMetrics:MTFontMathTable?) {
|
||||
// sets up the degree of the radical
|
||||
var kernBefore = fontMetrics!.radicalKernBeforeDegree;
|
||||
let kernAfter = fontMetrics!.radicalKernAfterDegree;
|
||||
let raise = fontMetrics!.radicalDegreeBottomRaisePercent * (self.ascent - self.descent);
|
||||
|
||||
// The layout is:
|
||||
// kernBefore, raise, degree, kernAfter, radical
|
||||
self.degree = degree;
|
||||
|
||||
// the radical is now shifted by kernBefore + degree.width + kernAfter
|
||||
_radicalShift = kernBefore + degree!.width + kernAfter;
|
||||
if _radicalShift < 0 {
|
||||
// we can't have the radical shift backwards, so instead we increase the kernBefore such
|
||||
// that _radicalShift will be 0.
|
||||
kernBefore -= _radicalShift;
|
||||
_radicalShift = 0;
|
||||
}
|
||||
|
||||
// Note: position of degree is relative to parent.
|
||||
self.degree!.position = CGPointMake(self.position.x + kernBefore, self.position.y + raise);
|
||||
// Update the width by the _radicalShift
|
||||
self.width = _radicalShift + _radicalGlyph!.width + self.radicand!.width;
|
||||
// update the position of the radicand
|
||||
self.updateRadicandPosition()
|
||||
}
|
||||
|
||||
func updateRadicandPosition() {
|
||||
// The position of the radicand includes the position of the MTRadicalDisplay
|
||||
// This is to make the positioning of the radical consistent with fractions and
|
||||
// have the cursor position finding algorithm work correctly.
|
||||
// move the radicand by the width of the radical sign
|
||||
self.radicand!.position = CGPointMake(self.position.x + _radicalShift + _radicalGlyph!.width, self.position.y);
|
||||
}
|
||||
|
||||
override public func draw(_ context: CGContext) {
|
||||
super.draw(context)
|
||||
|
||||
// draw the radicand & degree at its position
|
||||
self.radicand?.draw(context)
|
||||
self.degree?.draw(context)
|
||||
|
||||
context.saveGState();
|
||||
self.textColor?.setStroke()
|
||||
self.textColor?.setFill()
|
||||
|
||||
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
||||
context.translateBy(x: self.position.x + _radicalShift, y: self.position.y);
|
||||
context.textPosition = CGPoint.zero
|
||||
|
||||
// Draw the glyph.
|
||||
_radicalGlyph?.draw(context)
|
||||
|
||||
// Draw the VBOX
|
||||
// for the kern of, we don't need to draw anything.
|
||||
let heightFromTop = topKern;
|
||||
|
||||
// draw the horizontal line with the given thickness
|
||||
let path = MTBezierPath()
|
||||
let lineStart = CGPointMake(_radicalGlyph!.width, self.ascent - heightFromTop - self.lineThickness / 2); // subtract half the line thickness to center the line
|
||||
let lineEnd = CGPointMake(lineStart.x + self.radicand!.width, lineStart.y);
|
||||
path.move(to: lineStart)
|
||||
path.addLine(to: lineEnd)
|
||||
path.lineWidth = lineThickness
|
||||
path.lineCapStyle = .round
|
||||
path.stroke()
|
||||
|
||||
context.restoreGState();
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MTGlyphDisplay
|
||||
|
||||
/// Rendering a glyph as a display
|
||||
class MTGlyphDisplay : MTDisplayDS {
|
||||
|
||||
var glyph:CGGlyph!
|
||||
var font:MTFont?
|
||||
/// Horizontal scale factor for stretching glyphs (1.0 = no scaling)
|
||||
var scaleX: CGFloat = 1.0
|
||||
|
||||
init(withGlpyh glyph:CGGlyph, range:NSRange, font:MTFont?) {
|
||||
super.init()
|
||||
self.font = font
|
||||
self.glyph = glyph
|
||||
|
||||
self.position = CGPoint.zero
|
||||
self.range = range
|
||||
}
|
||||
|
||||
override public func draw(_ context: CGContext) {
|
||||
super.draw(context)
|
||||
context.saveGState()
|
||||
|
||||
self.textColor?.setFill()
|
||||
|
||||
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
||||
|
||||
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown);
|
||||
|
||||
// Apply horizontal scaling if needed (for stretchy arrows)
|
||||
if scaleX != 1.0 {
|
||||
context.scaleBy(x: scaleX, y: 1.0)
|
||||
}
|
||||
|
||||
context.textPosition = CGPoint.zero
|
||||
|
||||
var pos = CGPoint.zero
|
||||
CTFontDrawGlyphs(font!.ctFont, &glyph, &pos, 1, context);
|
||||
|
||||
context.restoreGState();
|
||||
}
|
||||
|
||||
override var ascent:CGFloat {
|
||||
set { super.ascent = newValue }
|
||||
get { super.ascent - self.shiftDown }
|
||||
}
|
||||
|
||||
override var descent:CGFloat {
|
||||
set { super.descent = newValue }
|
||||
get { super.descent + self.shiftDown }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MTGlyphConstructionDisplay
|
||||
|
||||
class MTGlyphConstructionDisplay:MTDisplayDS {
|
||||
var glyphs = [CGGlyph]()
|
||||
var positions = [CGPoint]()
|
||||
var font:MTFont?
|
||||
var numGlyphs:Int=0
|
||||
|
||||
init(withGlyphs glyphs:[NSNumber?], offsets:[NSNumber?], font:MTFont?) {
|
||||
super.init()
|
||||
assert(glyphs.count == offsets.count, "Glyphs and offsets need to match")
|
||||
self.numGlyphs = glyphs.count;
|
||||
self.glyphs = [CGGlyph](repeating: CGGlyph(), count: self.numGlyphs) //malloc(sizeof(CGGlyph) * _numGlyphs);
|
||||
self.positions = [CGPoint](repeating: CGPoint.zero, count: self.numGlyphs) //malloc(sizeof(CGPoint) * _numGlyphs);
|
||||
for i in 0 ..< self.numGlyphs {
|
||||
self.glyphs[i] = glyphs[i]!.uint16Value
|
||||
self.positions[i] = CGPointMake(0, CGFloat(offsets[i]!.floatValue))
|
||||
}
|
||||
self.font = font
|
||||
self.position = CGPoint.zero
|
||||
}
|
||||
|
||||
override public func draw(_ context: CGContext) {
|
||||
super.draw(context)
|
||||
context.saveGState()
|
||||
|
||||
self.textColor?.setFill()
|
||||
|
||||
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
|
||||
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown)
|
||||
context.textPosition = CGPoint.zero
|
||||
|
||||
// Draw the glyphs.
|
||||
CTFontDrawGlyphs(font!.ctFont, glyphs, positions, numGlyphs, context)
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
override var ascent:CGFloat {
|
||||
set { super.ascent = newValue }
|
||||
get { super.ascent - self.shiftDown }
|
||||
}
|
||||
|
||||
override var descent:CGFloat {
|
||||
set { super.descent = newValue }
|
||||
get { super.descent + self.shiftDown }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTLargeOpLimitsDisplay
|
||||
|
||||
/// Rendering a large operator with limits as an MTDisplay
|
||||
class MTLargeOpLimitsDisplay : MTDisplay {
|
||||
|
||||
/** A display representing the upper limit of the large operator. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
var upperLimit:MTMathListDisplay?
|
||||
/** A display representing the lower limit of the large operator. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
var lowerLimit:MTMathListDisplay?
|
||||
|
||||
var limitShift:CGFloat=0
|
||||
var upperLimitGap:CGFloat=0 { didSet { self.updateUpperLimitPosition() } }
|
||||
var lowerLimitGap:CGFloat=0 { didSet { self.updateLowerLimitPosition() } }
|
||||
var extraPadding:CGFloat=0
|
||||
|
||||
var nucleus:MTDisplay?
|
||||
|
||||
init(withNucleus nucleus:MTDisplay?, upperLimit:MTMathListDisplay?, lowerLimit:MTMathListDisplay?, limitShift:CGFloat, extraPadding:CGFloat) {
|
||||
super.init()
|
||||
self.upperLimit = upperLimit;
|
||||
self.lowerLimit = lowerLimit;
|
||||
self.nucleus = nucleus;
|
||||
|
||||
var maxWidth = max(nucleus!.width, upperLimit?.width ?? 0)
|
||||
maxWidth = max(maxWidth, lowerLimit?.width ?? 0)
|
||||
|
||||
self.limitShift = limitShift;
|
||||
self.upperLimitGap = 0;
|
||||
self.lowerLimitGap = 0;
|
||||
self.extraPadding = extraPadding; // corresponds to \xi_13 in TeX
|
||||
self.width = maxWidth;
|
||||
}
|
||||
|
||||
override var ascent:CGFloat {
|
||||
set { super.ascent = newValue }
|
||||
get {
|
||||
if self.upperLimit != nil {
|
||||
return nucleus!.ascent + extraPadding + self.upperLimit!.ascent + upperLimitGap + self.upperLimit!.descent
|
||||
} else {
|
||||
return nucleus!.ascent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var descent:CGFloat {
|
||||
set { super.descent = newValue }
|
||||
get {
|
||||
if self.lowerLimit != nil {
|
||||
return nucleus!.descent + extraPadding + lowerLimitGap + self.lowerLimit!.descent + self.lowerLimit!.ascent;
|
||||
} else {
|
||||
return nucleus!.descent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var position: CGPoint {
|
||||
set {
|
||||
super.position = newValue
|
||||
self.updateLowerLimitPosition()
|
||||
self.updateUpperLimitPosition()
|
||||
self.updateNucleusPosition()
|
||||
}
|
||||
get { super.position }
|
||||
}
|
||||
|
||||
func updateLowerLimitPosition() {
|
||||
if self.lowerLimit != nil {
|
||||
// The position of the lower limit includes the position of the MTLargeOpLimitsDisplay
|
||||
// This is to make the positioning of the radical consistent with fractions and radicals
|
||||
// Move the starting point to below the nucleus leaving a gap of _lowerLimitGap and subtract
|
||||
// the ascent to to get the baseline. Also center and shift it to the left by _limitShift.
|
||||
self.lowerLimit!.position = CGPointMake(self.position.x - limitShift + (self.width - lowerLimit!.width)/2,
|
||||
self.position.y - nucleus!.descent - lowerLimitGap - self.lowerLimit!.ascent);
|
||||
}
|
||||
}
|
||||
|
||||
func updateUpperLimitPosition() {
|
||||
if self.upperLimit != nil {
|
||||
// The position of the upper limit includes the position of the MTLargeOpLimitsDisplay
|
||||
// This is to make the positioning of the radical consistent with fractions and radicals
|
||||
// Move the starting point to above the nucleus leaving a gap of _upperLimitGap and add
|
||||
// the descent to to get the baseline. Also center and shift it to the right by _limitShift.
|
||||
self.upperLimit!.position = CGPointMake(self.position.x + limitShift + (self.width - self.upperLimit!.width)/2,
|
||||
self.position.y + nucleus!.ascent + upperLimitGap + self.upperLimit!.descent);
|
||||
}
|
||||
}
|
||||
|
||||
func updateNucleusPosition() {
|
||||
// Center the nucleus
|
||||
nucleus?.position = CGPointMake(self.position.x + (self.width - nucleus!.width)/2, self.position.y);
|
||||
}
|
||||
|
||||
override var textColor: MTColor? {
|
||||
set {
|
||||
super.textColor = newValue
|
||||
self.upperLimit?.textColor = newValue
|
||||
self.lowerLimit?.textColor = newValue
|
||||
nucleus?.textColor = newValue
|
||||
}
|
||||
get { super.textColor }
|
||||
}
|
||||
|
||||
override func draw(_ context:CGContext) {
|
||||
super.draw(context)
|
||||
// Draw the elements.
|
||||
self.upperLimit?.draw(context)
|
||||
self.lowerLimit?.draw(context)
|
||||
nucleus?.draw(context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTLineDisplay
|
||||
|
||||
/// Rendering of an list with an overline or underline
|
||||
class MTLineDisplay : MTDisplay {
|
||||
|
||||
/** A display representing the inner list that is underlined. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
var inner:MTMathListDisplay?
|
||||
var lineShiftUp:CGFloat=0
|
||||
var lineThickness:CGFloat=0
|
||||
|
||||
init(withInner inner:MTMathListDisplay?, position:CGPoint, range:NSRange) {
|
||||
super.init()
|
||||
self.inner = inner;
|
||||
|
||||
self.position = position;
|
||||
self.range = range;
|
||||
}
|
||||
|
||||
override var textColor: MTColor? {
|
||||
set {
|
||||
super.textColor = newValue
|
||||
inner?.textColor = newValue
|
||||
}
|
||||
get { super.textColor }
|
||||
}
|
||||
|
||||
override var position: CGPoint {
|
||||
set {
|
||||
super.position = newValue
|
||||
self.updateInnerPosition()
|
||||
}
|
||||
get { super.position }
|
||||
}
|
||||
|
||||
override func draw(_ context:CGContext) {
|
||||
super.draw(context)
|
||||
self.inner?.draw(context)
|
||||
|
||||
context.saveGState();
|
||||
|
||||
self.textColor?.setStroke()
|
||||
|
||||
// draw the horizontal line
|
||||
let path = MTBezierPath()
|
||||
let lineStart = CGPointMake(self.position.x, self.position.y + self.lineShiftUp);
|
||||
let lineEnd = CGPointMake(lineStart.x + self.inner!.width, lineStart.y);
|
||||
path.move(to:lineStart)
|
||||
path.addLine(to: lineEnd)
|
||||
path.lineWidth = self.lineThickness;
|
||||
path.stroke()
|
||||
|
||||
context.restoreGState();
|
||||
}
|
||||
|
||||
func updateInnerPosition() {
|
||||
self.inner?.position = CGPointMake(self.position.x, self.position.y);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MTAccentDisplay
|
||||
|
||||
/// Rendering an accent as a display
|
||||
class MTAccentDisplay : MTDisplay {
|
||||
|
||||
/** A display representing the inner list that is accented. Its position is relative
|
||||
to the parent is not treated as a sub-display.
|
||||
*/
|
||||
var accentee:MTMathListDisplay?
|
||||
|
||||
/** A display representing the accent. Its position is relative to the current display.
|
||||
*/
|
||||
var accent:MTGlyphDisplay?
|
||||
|
||||
init(withAccent glyph:MTGlyphDisplay?, accentee:MTMathListDisplay?, range:NSRange) {
|
||||
super.init()
|
||||
self.accent = glyph
|
||||
self.accentee = accentee
|
||||
self.accentee?.position = CGPoint.zero
|
||||
self.range = range
|
||||
}
|
||||
|
||||
override var textColor: MTColor? {
|
||||
set {
|
||||
super.textColor = newValue
|
||||
accentee?.textColor = newValue
|
||||
accent?.textColor = newValue
|
||||
}
|
||||
get { super.textColor }
|
||||
}
|
||||
|
||||
override var position: CGPoint {
|
||||
set {
|
||||
super.position = newValue
|
||||
self.updateAccenteePosition()
|
||||
}
|
||||
get { super.position }
|
||||
}
|
||||
|
||||
func updateAccenteePosition() {
|
||||
self.accentee?.position = CGPointMake(self.position.x, self.position.y);
|
||||
}
|
||||
|
||||
override func draw(_ context:CGContext) {
|
||||
super.draw(context)
|
||||
self.accentee?.draw(context)
|
||||
|
||||
context.saveGState();
|
||||
context.translateBy(x: self.position.x, y: self.position.y);
|
||||
context.textPosition = CGPoint.zero
|
||||
|
||||
self.accent?.draw(context)
|
||||
|
||||
context.restoreGState();
|
||||
}
|
||||
|
||||
}
|
||||
197
third-party/SwiftMath/Sources/MathRender/MTMathListIndex.swift
vendored
Executable file
197
third-party/SwiftMath/Sources/MathRender/MTMathListIndex.swift
vendored
Executable file
|
|
@ -0,0 +1,197 @@
|
|||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* An index that points to a particular character in the MTMathList. The index is a LinkedList that represents
|
||||
* a path from the beginning of the MTMathList to reach a particular atom in the list. The next node of the path
|
||||
* is represented by the subIndex. The path terminates when the subIndex is nil.
|
||||
*
|
||||
* If there is a subIndex, the subIndexType denotes what branch the path takes (i.e. superscript, subscript,
|
||||
* numerator, denominator etc.).
|
||||
* e.g in the expression 25^{2/4} the index of the character 4 is represented as:
|
||||
* (1, superscript) -> (0, denominator) -> (0, none)
|
||||
* This can be interpreted as start at index 1 (i.e. the 5) go up to the superscript.
|
||||
* Then look at index 0 (i.e. 2/4) and go to the denominator. Then look up index 0 (i.e. the 4) which this final
|
||||
* index.
|
||||
*
|
||||
* The level of an index is the number of nodes in the LinkedList to get to the final path.
|
||||
*/
|
||||
public class MTMathListIndex {
|
||||
|
||||
/**
|
||||
The type of the subindex.
|
||||
|
||||
The type of the subindex denotes what branch the path to the atom that this index points to takes.
|
||||
*/
|
||||
public enum MTMathListSubIndexType: Int {
|
||||
/// The index denotes the whole atom, subIndex is nil.
|
||||
case none = 0
|
||||
/// The position in the subindex is an index into the nucleus
|
||||
case nucleus
|
||||
/// The subindex indexes into the superscript.
|
||||
case superscript
|
||||
/// The subindex indexes into the subscript
|
||||
case ssubscript
|
||||
/// The subindex indexes into the numerator (only valid for fractions)
|
||||
case numerator
|
||||
/// The subindex indexes into the denominator (only valid for fractions)
|
||||
case denominator
|
||||
/// The subindex indexes into the radicand (only valid for radicals)
|
||||
case radicand
|
||||
/// The subindex indexes into the degree (only valid for radicals)
|
||||
case degree
|
||||
}
|
||||
|
||||
/// The index of the associated atom.
|
||||
var atomIndex: Int
|
||||
|
||||
/// The type of subindex, e.g. superscript, numerator etc.
|
||||
var subIndexType: MTMathListSubIndexType = .none
|
||||
|
||||
/// The index into the sublist.
|
||||
var subIndex: MTMathListIndex?
|
||||
|
||||
var finalIndex: Int {
|
||||
if self.subIndexType == .none {
|
||||
return self.atomIndex
|
||||
} else {
|
||||
return self.subIndex?.finalIndex ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the previous index if present. Returns `nil` if there is no previous index.
|
||||
func prevIndex() -> MTMathListIndex? {
|
||||
if self.subIndexType == .none {
|
||||
if self.atomIndex > 0 {
|
||||
return MTMathListIndex(level0Index: self.atomIndex - 1)
|
||||
}
|
||||
} else {
|
||||
if let prevSubIndex = self.subIndex?.prevIndex() {
|
||||
return MTMathListIndex(at: self.atomIndex, with: prevSubIndex, type: self.subIndexType)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Returns the next index.
|
||||
func nextIndex() -> MTMathListIndex {
|
||||
if self.subIndexType == .none {
|
||||
return MTMathListIndex(level0Index: self.atomIndex + 1)
|
||||
} else if self.subIndexType == .nucleus {
|
||||
return MTMathListIndex(at: self.atomIndex + 1, with: self.subIndex, type: self.subIndexType)
|
||||
} else {
|
||||
return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.nextIndex(), type: self.subIndexType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this index represents the beginning of a line. Note there may be multiple lines in a MTMathList,
|
||||
* e.g. a superscript or a fraction numerator. This returns true if the innermost subindex points to the beginning of a
|
||||
* line.
|
||||
*/
|
||||
func isBeginningOfLine() -> Bool { self.finalIndex == 0 }
|
||||
|
||||
func isAtSameLevel(with index: MTMathListIndex?) -> Bool {
|
||||
if self.subIndexType != index?.subIndexType {
|
||||
return false
|
||||
} else if self.subIndexType == .none {
|
||||
// No subindexes, they are at the same level.
|
||||
return true
|
||||
} else if (self.atomIndex != index?.atomIndex) {
|
||||
return false
|
||||
} else {
|
||||
return self.subIndex?.isAtSameLevel(with: index?.subIndex) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the type of the innermost sub index. */
|
||||
func finalSubIndexType() -> MTMathListSubIndexType {
|
||||
if self.subIndex?.subIndex != nil {
|
||||
return self.subIndex!.finalSubIndexType()
|
||||
} else {
|
||||
return self.subIndexType
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if any of the subIndexes of this index have the given type. */
|
||||
func hasSubIndex(ofType type: MTMathListSubIndexType) -> Bool {
|
||||
if self.subIndexType == type {
|
||||
return true
|
||||
} else {
|
||||
return self.subIndex?.hasSubIndex(ofType: type) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
func levelUp(with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) -> MTMathListIndex {
|
||||
if self.subIndexType == .none {
|
||||
return MTMathListIndex(at: self.atomIndex, with: subIndex, type: type)
|
||||
}
|
||||
|
||||
return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.levelUp(with: subIndex, type: type), type: self.subIndexType)
|
||||
}
|
||||
|
||||
func levelDown() -> MTMathListIndex? {
|
||||
if self.subIndexType == .none {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let subIndexDown = self.subIndex?.levelDown() {
|
||||
return MTMathListIndex(at: self.atomIndex, with: subIndexDown, type: self.subIndexType)
|
||||
} else {
|
||||
return MTMathListIndex(level0Index: self.atomIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/** Factory function to create a `MTMathListIndex` with no subindexes.
|
||||
@param index The index of the atom that the `MTMathListIndex` points at.
|
||||
*/
|
||||
public init(level0Index: Int) {
|
||||
self.atomIndex = level0Index
|
||||
}
|
||||
|
||||
public convenience init(at location: Int, with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) {
|
||||
self.init(level0Index: location)
|
||||
self.subIndexType = type
|
||||
self.subIndex = subIndex
|
||||
}
|
||||
}
|
||||
|
||||
extension MTMathListIndex: CustomStringConvertible {
|
||||
public var description: String {
|
||||
if self.subIndex != nil {
|
||||
return "[\(self.atomIndex), \(self.subIndexType.rawValue):\(self.subIndex!)]"
|
||||
}
|
||||
return "[\(self.atomIndex)]"
|
||||
}
|
||||
}
|
||||
|
||||
extension MTMathListIndex: Hashable {
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(self.atomIndex)
|
||||
hasher.combine(self.subIndexType)
|
||||
hasher.combine(self.subIndex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MTMathListIndex: Equatable {
|
||||
public static func ==(lhs: MTMathListIndex, rhs: MTMathListIndex) -> Bool {
|
||||
if lhs.atomIndex != rhs.atomIndex || lhs.subIndexType != rhs.subIndexType {
|
||||
return false
|
||||
}
|
||||
|
||||
if rhs.subIndex != nil {
|
||||
return rhs.subIndex == lhs.subIndex
|
||||
} else {
|
||||
return lhs.subIndex == nil
|
||||
}
|
||||
}
|
||||
}
|
||||
83
third-party/SwiftMath/Sources/MathRender/MTMathRenderer.swift
vendored
Normal file
83
third-party/SwiftMath/Sources/MathRender/MTMathRenderer.swift
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import Foundation
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public struct MTMathRenderedFormula {
|
||||
public let image: MTImage
|
||||
public let size: CGSize
|
||||
public let width: CGFloat
|
||||
public let ascent: CGFloat
|
||||
public let descent: CGFloat
|
||||
|
||||
public init(image: MTImage, size: CGSize, width: CGFloat, ascent: CGFloat, descent: CGFloat) {
|
||||
self.image = image
|
||||
self.size = size
|
||||
self.width = width
|
||||
self.ascent = ascent
|
||||
self.descent = descent
|
||||
}
|
||||
}
|
||||
|
||||
public enum MTMathRenderer {
|
||||
public static func render(latex: String, fontSize: CGFloat, textColor: MTColor, mode: MTMathUILabelMode = .display) -> MTMathRenderedFormula? {
|
||||
guard let font = MTFontManager.fontManager.defaultFont?.copy(withSize: fontSize) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var error: NSError?
|
||||
guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let style: MTLineStyle
|
||||
switch mode {
|
||||
case .display:
|
||||
style = .display
|
||||
case .text:
|
||||
style = .text
|
||||
}
|
||||
|
||||
guard let displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: style) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
displayList.textColor = textColor
|
||||
|
||||
let width = max(1.0, ceil(displayList.width))
|
||||
let height = max(1.0, ceil(displayList.ascent + displayList.descent))
|
||||
let size = CGSize(width: width, height: height)
|
||||
displayList.position = CGPoint(x: 0.0, y: displayList.descent)
|
||||
|
||||
#if os(iOS) || os(visionOS)
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
let image = renderer.image { rendererContext in
|
||||
rendererContext.cgContext.saveGState()
|
||||
var transform = CGAffineTransform(scaleX: 1.0, y: -1.0)
|
||||
transform = transform.translatedBy(x: 0.0, y: -size.height)
|
||||
rendererContext.cgContext.concatenate(transform)
|
||||
displayList.draw(rendererContext.cgContext)
|
||||
rendererContext.cgContext.restoreGState()
|
||||
}
|
||||
return MTMathRenderedFormula(image: image, size: size, width: displayList.width, ascent: displayList.ascent, descent: displayList.descent)
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
let image = NSImage(size: size, flipped: false) { _ in
|
||||
guard let context = NSGraphicsContext.current?.cgContext else {
|
||||
return false
|
||||
}
|
||||
context.saveGState()
|
||||
displayList.draw(context)
|
||||
context.restoreGState()
|
||||
return true
|
||||
}
|
||||
return MTMathRenderedFormula(image: image, size: size, width: displayList.width, ascent: displayList.ascent, descent: displayList.descent)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
587
third-party/SwiftMath/Sources/MathRender/MTMathUILabel.swift
vendored
Normal file
587
third-party/SwiftMath/Sources/MathRender/MTMathUILabel.swift
vendored
Normal file
|
|
@ -0,0 +1,587 @@
|
|||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreText
|
||||
|
||||
/**
|
||||
Different display styles supported by the `MTMathUILabel`.
|
||||
|
||||
The only significant difference between the two modes is how fractions
|
||||
and limits on large operators are displayed.
|
||||
*/
|
||||
public enum MTMathUILabelMode {
|
||||
/// Display mode. Equivalent to $$ in TeX
|
||||
case display
|
||||
/// Text mode. Equivalent to $ in TeX.
|
||||
case text
|
||||
}
|
||||
|
||||
/**
|
||||
Horizontal text alignment for `MTMathUILabel`.
|
||||
*/
|
||||
public enum MTTextAlignment : UInt {
|
||||
/// Align left.
|
||||
case left
|
||||
/// Align center.
|
||||
case center
|
||||
/// Align right.
|
||||
case right
|
||||
}
|
||||
|
||||
/** The main view for rendering math.
|
||||
|
||||
`MTMathLabel` accepts either a string in LaTeX or an `MTMathList` to display. Use
|
||||
`MTMathList` directly only if you are building it programmatically (e.g. using an
|
||||
editor), otherwise using LaTeX is the preferable method.
|
||||
|
||||
The math display is centered vertically in the label. The default horizontal alignment is
|
||||
is left. This can be changed by setting `textAlignment`. The math is default displayed in
|
||||
*Display* mode. This can be changed using `labelMode`.
|
||||
|
||||
When created it uses `[MTFontManager defaultFont]` as its font. This can be changed using
|
||||
the `font` parameter.
|
||||
*/
|
||||
@IBDesignable
|
||||
public class MTMathUILabel : MTView {
|
||||
|
||||
/** The `MTMathList` to render. Setting this will remove any
|
||||
`latex` that has already been set. If `latex` has been set, this will
|
||||
return the parsed `MTMathList` if the `latex` parses successfully. Use this
|
||||
setting if the `MTMathList` has been programmatically constructed, otherwise it
|
||||
is preferred to use `latex`.
|
||||
*/
|
||||
public var mathList:MTMathList? {
|
||||
set {
|
||||
_mathList = newValue
|
||||
_error = nil
|
||||
_latex = MTMathListBuilder.mathListToString(newValue)
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
get { _mathList }
|
||||
}
|
||||
private var _mathList:MTMathList?
|
||||
|
||||
/** The latex string to be displayed. Setting this will remove any `mathList` that
|
||||
has been set. If latex has not been set, this will return the latex output for the
|
||||
`mathList` that is set.
|
||||
@see error */
|
||||
@IBInspectable
|
||||
public var latex:String {
|
||||
set {
|
||||
_latex = newValue
|
||||
_error = nil
|
||||
var error : NSError? = nil
|
||||
_mathList = MTMathListBuilder.build(fromString: newValue, error: &error)
|
||||
if error != nil {
|
||||
_mathList = nil
|
||||
_error = error
|
||||
self.errorLabel?.text = error!.localizedDescription
|
||||
self.errorLabel?.frame = self.bounds
|
||||
self.errorLabel?.isHidden = !self.displayErrorInline
|
||||
} else {
|
||||
self.errorLabel?.isHidden = true
|
||||
}
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
get { _latex }
|
||||
}
|
||||
private var _latex = ""
|
||||
|
||||
/** This contains any error that occurred when parsing the latex. */
|
||||
public var error:NSError? { _error }
|
||||
private var _error:NSError?
|
||||
|
||||
/** If true, if there is an error it displays the error message inline. Default true. */
|
||||
public var displayErrorInline = true
|
||||
|
||||
/** If true, enables detailed debug logging for intrinsicContentSize calculations.
|
||||
This helps diagnose rendering issues like missing row breaks in aligned environments.
|
||||
Default false. */
|
||||
public var debugLogging = false
|
||||
|
||||
/** The MTFont to use for rendering. */
|
||||
public var font:MTFont? {
|
||||
set {
|
||||
guard newValue != nil else { return }
|
||||
_font = newValue
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
get { _font }
|
||||
}
|
||||
private var _font:MTFont?
|
||||
|
||||
/** Convenience method to just set the size of the font without changing the fontface. */
|
||||
@IBInspectable
|
||||
public var fontSize:CGFloat {
|
||||
set {
|
||||
_fontSize = newValue
|
||||
let font = font?.copy(withSize: newValue)
|
||||
self.font = font // also forces an update
|
||||
}
|
||||
get { _fontSize }
|
||||
}
|
||||
private var _fontSize:CGFloat=0
|
||||
|
||||
/** This sets the text color of the rendered math formula. The default color is black. */
|
||||
@IBInspectable
|
||||
public var textColor:MTColor? {
|
||||
set {
|
||||
guard newValue != nil else { return }
|
||||
_textColor = newValue
|
||||
self.displayList?.textColor = newValue
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
get { _textColor }
|
||||
}
|
||||
private var _textColor:MTColor?
|
||||
|
||||
/** The minimum distance from the margin of the view to the rendered math. This value is
|
||||
`UIEdgeInsetsZero` by default. This is useful if you need some padding between the math and
|
||||
the border/background color. sizeThatFits: will have its returned size increased by these insets.
|
||||
*/
|
||||
@IBInspectable
|
||||
public var contentInsets:MTEdgeInsets {
|
||||
set {
|
||||
_contentInsets = newValue
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
get { _contentInsets }
|
||||
}
|
||||
private var _contentInsets = MTEdgeInsetsZero
|
||||
|
||||
/** The Label mode for the label. The default mode is Display */
|
||||
public var labelMode:MTMathUILabelMode {
|
||||
set {
|
||||
_labelMode = newValue
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
get { _labelMode }
|
||||
}
|
||||
private var _labelMode = MTMathUILabelMode.display
|
||||
|
||||
/** Horizontal alignment for the text. The default is align left. */
|
||||
public var textAlignment:MTTextAlignment {
|
||||
set {
|
||||
_textAlignment = newValue
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
get { _textAlignment }
|
||||
}
|
||||
private var _textAlignment = MTTextAlignment.left
|
||||
|
||||
/** The internal display of the MTMathUILabel. This is for advanced use only. */
|
||||
public var displayList: MTMathListDisplay? { _displayList }
|
||||
private var _displayList:MTMathListDisplay?
|
||||
|
||||
/** The preferred maximum width (in points) for a multiline label.
|
||||
Set this property to enable line wrapping based on available width. */
|
||||
public var preferredMaxLayoutWidth: CGFloat {
|
||||
set {
|
||||
_preferredMaxLayoutWidth = newValue
|
||||
_displayList = nil // Clear cached display list when width constraint changes
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
get { _preferredMaxLayoutWidth }
|
||||
}
|
||||
private var _preferredMaxLayoutWidth: CGFloat = 0
|
||||
|
||||
public var currentStyle:MTLineStyle {
|
||||
switch _labelMode {
|
||||
case .display: return .display
|
||||
case .text: return .text
|
||||
}
|
||||
}
|
||||
|
||||
public var errorLabel: MTLabel?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.initCommon()
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
self.initCommon()
|
||||
}
|
||||
|
||||
func initCommon() {
|
||||
#if os(macOS)
|
||||
self.layer?.isGeometryFlipped = true
|
||||
#else
|
||||
self.layer.isGeometryFlipped = true
|
||||
self.clipsToBounds = true
|
||||
#endif
|
||||
_fontSize = 20
|
||||
_contentInsets = MTEdgeInsetsZero
|
||||
_labelMode = .display
|
||||
let font = MTFontManager.fontManager.defaultFont
|
||||
self.font = font
|
||||
_textAlignment = .left
|
||||
_displayList = nil
|
||||
displayErrorInline = true
|
||||
self.backgroundColor = MTColor.clear
|
||||
|
||||
_textColor = MTColor.black
|
||||
let label = MTLabel()
|
||||
self.errorLabel = label
|
||||
#if os(macOS)
|
||||
label.layer?.isGeometryFlipped = true
|
||||
#else
|
||||
label.layer.isGeometryFlipped = true
|
||||
#endif
|
||||
label.isHidden = true
|
||||
label.textColor = MTColor.red
|
||||
self.addSubview(label)
|
||||
}
|
||||
|
||||
override public func draw(_ dirtyRect: MTRect) {
|
||||
super.draw(dirtyRect)
|
||||
if self.mathList == nil { return }
|
||||
if self.font == nil { return }
|
||||
|
||||
// Ensure display list is created before drawing
|
||||
if _displayList == nil {
|
||||
_layoutSubviews()
|
||||
}
|
||||
|
||||
guard let displayList = _displayList else { return }
|
||||
|
||||
// drawing code
|
||||
let context = MTGraphicsGetCurrentContext()!
|
||||
context.saveGState()
|
||||
|
||||
// CRITICAL FIX for clipping: If the displayList is wider than our bounds,
|
||||
// expand the clipping rect to prevent content clipping.
|
||||
// This handles cases where preferredMaxLayoutWidth is a hint but the content
|
||||
// cannot fit within it even with line breaking.
|
||||
let contentWidth = displayList.width + contentInsets.left + contentInsets.right
|
||||
if contentWidth > bounds.size.width {
|
||||
// Content is wider than bounds - expand clip rect
|
||||
let expandedRect = CGRect(
|
||||
x: bounds.origin.x,
|
||||
y: bounds.origin.y,
|
||||
width: contentWidth,
|
||||
height: bounds.size.height
|
||||
)
|
||||
context.clip(to: expandedRect)
|
||||
}
|
||||
|
||||
displayList.draw(context)
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
func _layoutSubviews() {
|
||||
guard _mathList != nil && self.font != nil else {
|
||||
_displayList = nil
|
||||
errorLabel?.frame = self.bounds
|
||||
self.setNeedsDisplay()
|
||||
return
|
||||
}
|
||||
// Ensure we have a valid font before attempting to typeset
|
||||
if self.font == nil {
|
||||
// No valid font - try to get default font
|
||||
if let defaultFont = MTFontManager.fontManager.defaultFont {
|
||||
self._font = defaultFont
|
||||
} else {
|
||||
// Cannot typeset without a font, clear display list
|
||||
_displayList = nil
|
||||
errorLabel?.frame = self.bounds
|
||||
self.setNeedsDisplay()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Use the effective width for layout
|
||||
let effectiveWidth = _preferredMaxLayoutWidth > 0 ? _preferredMaxLayoutWidth : bounds.size.width
|
||||
var availableWidth = effectiveWidth - contentInsets.left - contentInsets.right
|
||||
// CRITICAL FIX: Ensure availableWidth is never negative
|
||||
// Negative maxWidth passed to MTTypesetter can cause "Negative value is not representable" crashes
|
||||
availableWidth = max(0, availableWidth)
|
||||
|
||||
_displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: availableWidth)
|
||||
|
||||
guard let displayList = _displayList else {
|
||||
// Empty or invalid input - nothing to display
|
||||
return
|
||||
}
|
||||
|
||||
displayList.textColor = textColor
|
||||
var textX = CGFloat(0)
|
||||
switch self.textAlignment {
|
||||
case .left: textX = contentInsets.left
|
||||
case .center: textX = (bounds.size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
|
||||
case .right: textX = bounds.size.width - displayList.width - contentInsets.right
|
||||
}
|
||||
let availableHeight = bounds.size.height - contentInsets.bottom - contentInsets.top
|
||||
|
||||
// center things vertically
|
||||
var height = displayList.ascent + displayList.descent
|
||||
if height < fontSize/2 {
|
||||
height = fontSize/2 // set height to half the font size
|
||||
}
|
||||
let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom
|
||||
|
||||
displayList.position = CGPointMake(textX, textY)
|
||||
errorLabel?.frame = self.bounds
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
|
||||
func _sizeThatFits(_ size:CGSize) -> CGSize {
|
||||
// Check if we have empty latex (empty string case)
|
||||
if _latex.isEmpty {
|
||||
// Empty latex - return zero size
|
||||
return CGSize(width: 0, height: 0)
|
||||
}
|
||||
|
||||
guard _mathList != nil else {
|
||||
// No content - return no-intrinsic-size marker
|
||||
return CGSize(width: -1, height: -1)
|
||||
}
|
||||
|
||||
// Ensure we have a valid font before attempting to typeset
|
||||
if self.font == nil {
|
||||
// No valid font - try to get default font
|
||||
if let defaultFont = MTFontManager.fontManager.defaultFont {
|
||||
self._font = defaultFont
|
||||
} else {
|
||||
// Cannot typeset without a font
|
||||
return CGSize(width: -1, height: -1)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the maximum width to use
|
||||
var maxWidth: CGFloat = 0
|
||||
if _preferredMaxLayoutWidth > 0 {
|
||||
maxWidth = _preferredMaxLayoutWidth - contentInsets.left - contentInsets.right
|
||||
// CRITICAL FIX: Ensure maxWidth is never negative
|
||||
// If contentInsets exceed available width, clamp to 0
|
||||
maxWidth = max(0, maxWidth)
|
||||
} else if size.width > 0 {
|
||||
maxWidth = size.width - contentInsets.left - contentInsets.right
|
||||
// CRITICAL FIX: Ensure maxWidth is never negative
|
||||
maxWidth = max(0, maxWidth)
|
||||
}
|
||||
|
||||
var displayList:MTMathListDisplay? = nil
|
||||
displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: maxWidth)
|
||||
|
||||
guard displayList != nil else {
|
||||
// Failed to create display list
|
||||
return CGSize(width: -1, height: -1)
|
||||
}
|
||||
|
||||
var resultWidth = displayList!.width + contentInsets.left + contentInsets.right
|
||||
var resultHeight = displayList!.ascent + displayList!.descent + contentInsets.top + contentInsets.bottom
|
||||
|
||||
// DEBUG LOGGING for width calculation
|
||||
if debugLogging {
|
||||
print("\n=== MTMathUILabel intrinsicContentSize DEBUG ===")
|
||||
print("LaTeX: \(self.latex)")
|
||||
// Show raw bytes to help debug escaping issues
|
||||
let latexBytes = Array(self.latex.utf8.prefix(100))
|
||||
print("LaTeX bytes (first 100): \(latexBytes)")
|
||||
print("preferredMaxLayoutWidth: \(_preferredMaxLayoutWidth)")
|
||||
print("size constraint: \(size)")
|
||||
print("maxWidth passed to typesetter: \(maxWidth)")
|
||||
print("displayList.width: \(displayList!.width)")
|
||||
print("displayList.ascent: \(displayList!.ascent)")
|
||||
print("displayList.descent: \(displayList!.descent)")
|
||||
print("Number of subDisplays: \(displayList!.subDisplays.count)")
|
||||
// Count lines by unique Y positions
|
||||
let yPositions = Set(displayList!.subDisplays.map { $0.position.y })
|
||||
print("Number of lines (unique Y positions): \(yPositions.count)")
|
||||
print("contentInsets: \(contentInsets)")
|
||||
print("resultWidth (before clamping): \(resultWidth)")
|
||||
print("resultHeight (before clamping): \(resultHeight)")
|
||||
|
||||
// HELPFUL WARNING: Detect common escaping mistake
|
||||
// Check for environments that typically have multiple rows but only 1 row is rendered
|
||||
let multiRowEnvs = ["aligned", "align", "eqnarray", "gather", "split"]
|
||||
let latexLower = self.latex.lowercased()
|
||||
for env in multiRowEnvs {
|
||||
if latexLower.contains("\\begin{\(env)}") {
|
||||
// Check if there's a table with only 1 row
|
||||
if let ml = _mathList {
|
||||
for atom in ml.atoms {
|
||||
if let table = atom as? MTMathTable, table.numRows == 1 {
|
||||
// Check if latex contains single backslash (spacing) where double might be intended
|
||||
// Pattern: \\ followed by non-backslash (like space or letter)
|
||||
if self.latex.contains("\\ ") || self.latex.range(of: #"\\[a-zA-Z]"#, options: .regularExpression) != nil {
|
||||
print("⚠️ WARNING: '\(env)' environment has only 1 row.")
|
||||
print(" If you intended multiple rows, use '\\\\\\\\' (4 backslashes in Swift strings)")
|
||||
print(" or use raw strings: #\"...42\\\\ \\\\dfrac...\"#")
|
||||
print(" Current input has '\\\\ ' which is a SPACING command, not a row break.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Ensure dimensions are never negative
|
||||
// Negative values cause crashes in NSRange calculations and SwiftUI layout
|
||||
resultWidth = max(0, resultWidth)
|
||||
resultHeight = max(0, resultHeight)
|
||||
|
||||
// CRITICAL FIX for accented character clipping:
|
||||
// The preferredMaxLayoutWidth is a HINT for line breaking, NOT a hard constraint.
|
||||
// If the typesetter cannot fit content within that width (even with line breaking),
|
||||
// we MUST return the actual content width to prevent clipping.
|
||||
//
|
||||
// ONLY clamp in extreme cases to prevent layout explosion (>50% over or >100pt over)
|
||||
if _preferredMaxLayoutWidth > 0 && resultWidth > _preferredMaxLayoutWidth {
|
||||
let overflow = resultWidth - _preferredMaxLayoutWidth
|
||||
let overflowPercent = (overflow / _preferredMaxLayoutWidth) * 100
|
||||
|
||||
if debugLogging {
|
||||
print(" Content exceeds preferredMaxLayoutWidth:")
|
||||
print(" preferredMaxLayoutWidth: \(_preferredMaxLayoutWidth)")
|
||||
print(" resultWidth: \(resultWidth)")
|
||||
print(" overflow: \(overflow) (\(String(format: "%.1f", overflowPercent))%)")
|
||||
|
||||
// Check line breaking
|
||||
let yPositions = Set(displayList!.subDisplays.map { $0.position.y })
|
||||
print(" hasMultipleLines: \(yPositions.count > 1) (yPositions: \(yPositions.count))")
|
||||
|
||||
// Check if any content would be clipped at different widths
|
||||
print(" SubDisplay analysis:")
|
||||
for (i, sub) in displayList!.subDisplays.enumerated() {
|
||||
let rightEdge = sub.position.x + sub.width
|
||||
let clippedAtPreferred = rightEdge > _preferredMaxLayoutWidth
|
||||
let clippedAtResult = rightEdge > resultWidth
|
||||
print(" Sub[\(i)]: rightEdge=\(rightEdge) clippedAt\(_preferredMaxLayoutWidth)=\(clippedAtPreferred) clippedAt\(resultWidth)=\(clippedAtResult)")
|
||||
}
|
||||
}
|
||||
|
||||
// ONLY clamp for truly excessive overflow (>50% or >100pt)
|
||||
// This prevents layout explosion while allowing normal overflow
|
||||
let extremeOverflowThreshold: CGFloat = max(_preferredMaxLayoutWidth * 0.5, 100.0)
|
||||
|
||||
if overflow > extremeOverflowThreshold {
|
||||
// Extreme overflow - clamp to prevent layout issues
|
||||
let clampedWidth = _preferredMaxLayoutWidth + extremeOverflowThreshold
|
||||
if debugLogging {
|
||||
print(" ⚠️ EXTREME OVERFLOW - clamping from \(resultWidth) to \(clampedWidth)")
|
||||
print(" ⚠️ WARNING: This will cause content clipping!")
|
||||
}
|
||||
resultWidth = clampedWidth
|
||||
} else {
|
||||
// Normal overflow - keep actual content width to prevent clipping
|
||||
if debugLogging {
|
||||
print(" ✓ Normal overflow - keeping actual width \(resultWidth) to prevent clipping")
|
||||
}
|
||||
// resultWidth stays as is - NO CLAMPING
|
||||
}
|
||||
} else if _preferredMaxLayoutWidth == 0 && size.width > 0 && resultWidth > size.width {
|
||||
// Similar tolerance for size.width constraint
|
||||
let tolerance = max(size.width * 0.05, 10.0)
|
||||
let maxAllowedWidth = size.width + tolerance
|
||||
|
||||
if debugLogging {
|
||||
print(" Exceeds size.width constraint:")
|
||||
print(" tolerance: \(tolerance)")
|
||||
print(" maxAllowedWidth: \(maxAllowedWidth)")
|
||||
print(" overflow amount: \(resultWidth - size.width)")
|
||||
}
|
||||
|
||||
if resultWidth <= maxAllowedWidth {
|
||||
// Within tolerance - use actual content width
|
||||
// resultWidth stays as is
|
||||
if debugLogging {
|
||||
print(" ✓ Within tolerance - keeping actual width")
|
||||
}
|
||||
} else {
|
||||
if debugLogging {
|
||||
print(" ⚠️ CLAMPING to maxAllowedWidth (may clip content!)")
|
||||
}
|
||||
resultWidth = maxAllowedWidth
|
||||
}
|
||||
}
|
||||
|
||||
if debugLogging {
|
||||
print(" Final resultWidth: \(resultWidth)")
|
||||
print(" Final resultHeight: \(resultHeight)")
|
||||
|
||||
// Check if any display elements would be clipped
|
||||
if let display = displayList {
|
||||
print(" Display subdisplays: \(display.subDisplays.count)")
|
||||
let yPositions = Set(display.subDisplays.map { $0.position.y }).sorted()
|
||||
print(" Unique Y positions (lines): \(yPositions.count) -> \(yPositions)")
|
||||
|
||||
for (i, sub) in display.subDisplays.enumerated() {
|
||||
let rightEdge = sub.position.x + sub.width
|
||||
let clipped = rightEdge > resultWidth
|
||||
|
||||
// Extract text content if this is a CTLineDisplay
|
||||
var textContent = ""
|
||||
if let ctLineDisplay = sub as? MTCTLineDisplay,
|
||||
let attrString = ctLineDisplay.attributedString {
|
||||
textContent = " text=\"\(attrString.string)\""
|
||||
}
|
||||
|
||||
print(" Sub[\(i)]: type=\(type(of: sub)), y=\(sub.position.y), x=\(sub.position.x), width=\(sub.width), rightEdge=\(rightEdge)\(textContent)\(clipped ? " ⚠️ CLIPPED" : "")")
|
||||
|
||||
// Show internal structure for MTMathListDisplay
|
||||
if let mathListDisplay = sub as? MTMathListDisplay, !mathListDisplay.subDisplays.isEmpty {
|
||||
print(" → Contains \(mathListDisplay.subDisplays.count) sub-displays:")
|
||||
for (j, innerSub) in mathListDisplay.subDisplays.enumerated() {
|
||||
var innerTextContent = ""
|
||||
if let innerCTLineDisplay = innerSub as? MTCTLineDisplay,
|
||||
let innerAttrString = innerCTLineDisplay.attributedString {
|
||||
innerTextContent = " text=\"\(innerAttrString.string)\""
|
||||
}
|
||||
print(" [\(j)]: type=\(type(of: innerSub)), y=\(innerSub.position.y), x=\(innerSub.position.x), width=\(innerSub.width)\(innerTextContent)")
|
||||
}
|
||||
}
|
||||
|
||||
if clipped {
|
||||
print(" ⚠️ CLIPPING: rightEdge \(rightEdge) > resultWidth \(resultWidth)")
|
||||
print(" Clipped amount: \(rightEdge - resultWidth)")
|
||||
}
|
||||
}
|
||||
}
|
||||
print("=== END DEBUG ===\n")
|
||||
}
|
||||
|
||||
return CGSize(width: resultWidth, height: resultHeight)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
public func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return _sizeThatFits(size)
|
||||
}
|
||||
#else
|
||||
public override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return _sizeThatFits(size)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
func setNeedsDisplay() { self.needsDisplay = true }
|
||||
func setNeedsLayout() { self.needsLayout = true }
|
||||
public override var fittingSize: CGSize { _sizeThatFits(CGSizeZero) }
|
||||
public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
|
||||
override public func layout() {
|
||||
self._layoutSubviews()
|
||||
super.layout()
|
||||
}
|
||||
#else
|
||||
public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
|
||||
override public func layoutSubviews() { _layoutSubviews() }
|
||||
#endif
|
||||
|
||||
}
|
||||
68
third-party/SwiftMath/Sources/MathRender/MTTypesetter+Tokenization.swift
vendored
Normal file
68
third-party/SwiftMath/Sources/MathRender/MTTypesetter+Tokenization.swift
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// MTTypesetter+Tokenization.swift
|
||||
// SwiftMath
|
||||
//
|
||||
// Created by Claude Code on 2025-12-16.
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
extension MTTypesetter {
|
||||
|
||||
/// Create a line for a math list using the new tokenization approach
|
||||
/// This is an alternative to the existing createLineForMathList that uses
|
||||
/// pre-tokenization and greedy line fitting
|
||||
static func createLineForMathListWithTokenization(
|
||||
_ mathList: MTMathList?,
|
||||
font: MTFont?,
|
||||
style: MTLineStyle,
|
||||
cramped: Bool,
|
||||
spaced: Bool,
|
||||
maxWidth: CGFloat
|
||||
) -> MTMathListDisplay? {
|
||||
guard let mathList = mathList else { return nil }
|
||||
guard let font = font else { return nil }
|
||||
guard !mathList.atoms.isEmpty else {
|
||||
// Return empty display instead of nil (matches KaTeX behavior)
|
||||
return MTMathListDisplay(withDisplays: [], range: NSMakeRange(0, 0))
|
||||
}
|
||||
|
||||
// Phase 0: Preprocess atoms to fuse ordinary characters
|
||||
// This is critical for accents and other structures where multi-character
|
||||
// text like "xyzw" should stay together as a single atom
|
||||
let preprocessedAtoms = MTTypesetter.preprocessMathList(mathList)
|
||||
|
||||
// Phase 1: Tokenize atoms into breakable elements
|
||||
let tokenizer = MTAtomTokenizer(font: font, style: style, cramped: cramped, maxWidth: maxWidth)
|
||||
let elements = tokenizer.tokenize(preprocessedAtoms)
|
||||
|
||||
guard !elements.isEmpty else { return nil }
|
||||
|
||||
// Phase 2: Fit elements into lines
|
||||
let margin = spaced ? font.mathTable?.muUnit ?? 0 : 0
|
||||
let fitter = MTLineFitter(maxWidth: maxWidth, margin: margin)
|
||||
let fittedLines = fitter.fitLines(elements)
|
||||
|
||||
// Phase 3: Generate displays from fitted lines
|
||||
let generator = MTDisplayGenerator(font: font, style: style)
|
||||
let displays = generator.generateDisplays(from: fittedLines, startPosition: CGPoint.zero)
|
||||
|
||||
// Determine range from atoms
|
||||
let range: NSRange
|
||||
if let firstAtom = mathList.atoms.first, let lastAtom = mathList.atoms.last {
|
||||
let start = firstAtom.indexRange.location
|
||||
let end = NSMaxRange(lastAtom.indexRange)
|
||||
range = NSMakeRange(start, end - start)
|
||||
} else {
|
||||
range = NSMakeRange(0, 0)
|
||||
}
|
||||
|
||||
// Create and return the math list display
|
||||
let mathListDisplay = MTMathListDisplay(withDisplays: displays, range: range)
|
||||
|
||||
return mathListDisplay
|
||||
}
|
||||
}
|
||||
1927
third-party/SwiftMath/Sources/MathRender/MTTypesetter.swift
vendored
Normal file
1927
third-party/SwiftMath/Sources/MathRender/MTTypesetter.swift
vendored
Normal file
File diff suppressed because it is too large
Load diff
89
third-party/SwiftMath/Sources/MathRender/MTUnicode.swift
vendored
Executable file
89
third-party/SwiftMath/Sources/MathRender/MTUnicode.swift
vendored
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
//
|
||||
// Created by Mike Griebling on 2022-12-31.
|
||||
// Translated from an Objective-C implementation by Kostub Deshmukh.
|
||||
//
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct UnicodeSymbol {
|
||||
static let multiplication = "\u{00D7}"
|
||||
static let division = "\u{00F7}"
|
||||
static let fractionSlash = "\u{2044}"
|
||||
static let whiteSquare = "\u{25A1}"
|
||||
static let blackSquare = "\u{25A0}"
|
||||
static let lessEqual = "\u{2264}"
|
||||
static let greaterEqual = "\u{2265}"
|
||||
static let notEqual = "\u{2260}"
|
||||
static let squareRoot = "\u{221A}" // \sqrt
|
||||
static let cubeRoot = "\u{221B}"
|
||||
static let infinity = "\u{221E}" // \infty
|
||||
static let angle = "\u{2220}" // \angle
|
||||
static let degree = "\u{00B0}" // \circ
|
||||
|
||||
static let capitalGreekStart = UInt32(0x0391)
|
||||
static let capitalGreekEnd = UInt32(0x03A9)
|
||||
static let lowerGreekStart = UInt32(0x03B1)
|
||||
static let lowerGreekEnd = UInt32(0x03C9)
|
||||
static let planksConstant = UInt32(0x210e)
|
||||
static let lowerItalicStart = UInt32(0x1D44E)
|
||||
static let capitalItalicStart = UInt32(0x1D434)
|
||||
static let greekLowerItalicStart = UInt32(0x1D6FC)
|
||||
static let greekCapitalItalicStart = UInt32(0x1D6E2)
|
||||
static let greekSymbolItalicStart = UInt32(0x1D716)
|
||||
|
||||
static let mathCapitalBoldStart = UInt32(0x1D400)
|
||||
static let mathLowerBoldStart = UInt32(0x1D41A)
|
||||
static let greekCapitalBoldStart = UInt32(0x1D6A8)
|
||||
static let greekLowerBoldStart = UInt32(0x1D6C2)
|
||||
static let greekSymbolBoldStart = UInt32(0x1D6DC)
|
||||
static let numberBoldStart = UInt32(0x1D7CE)
|
||||
|
||||
static let mathCapitalBoldItalicStart = UInt32(0x1D468)
|
||||
static let mathLowerBoldItalicStart = UInt32(0x1D482)
|
||||
static let greekCapitalBoldItalicStart = UInt32(0x1D71C)
|
||||
static let greekLowerBoldItalicStart = UInt32(0x1D736)
|
||||
static let greekSymbolBoldItalicStart = UInt32(0x1D750)
|
||||
|
||||
static let mathCapitalScriptStart = UInt32(0x1D49C)
|
||||
static let mathCapitalTTStart = UInt32(0x1D670)
|
||||
static let mathLowerTTStart = UInt32(0x1D68A)
|
||||
static let numberTTStart = UInt32(0x1D7F6)
|
||||
static let mathCapitalSansSerifStart = UInt32(0x1D5A0)
|
||||
static let mathLowerSansSerifStart = UInt32(0x1D5BA)
|
||||
static let numberSansSerifStart = UInt32(0x1D7E2)
|
||||
static let mathCapitalFrakturStart = UInt32(0x1D504)
|
||||
static let mathLowerFrakturStart = UInt32(0x1D51E)
|
||||
static let mathCapitalBlackboardStart = UInt32(0x1D538)
|
||||
static let mathLowerBlackboardStart = UInt32(0x1D552)
|
||||
static let numberBlackboardStart = UInt32(0x1D7D8)
|
||||
}
|
||||
|
||||
extension Character {
|
||||
|
||||
var utf32Char: UTF32Char { self.unicodeScalars.map { $0.value }.reduce(0, +) }
|
||||
var isLowerEnglish : Bool { self >= "a" && self <= "z" }
|
||||
var isUpperEnglish : Bool { self >= "A" && self <= "Z" }
|
||||
var isNumber : Bool { self >= "0" && self <= "9" }
|
||||
|
||||
var isLowerGreek : Bool {
|
||||
let uch = self.utf32Char
|
||||
return uch >= UnicodeSymbol.lowerGreekStart && uch <= UnicodeSymbol.lowerGreekEnd
|
||||
}
|
||||
|
||||
var isCapitalGreek : Bool {
|
||||
let uch = self.utf32Char
|
||||
return uch >= UnicodeSymbol.capitalGreekStart && uch <= UnicodeSymbol.capitalGreekEnd
|
||||
}
|
||||
|
||||
var greekSymbolOrder : UInt32? {
|
||||
let greekSymbols : [UTF32Char] = [0x03F5, 0x03D1, 0x03F0, 0x03D5, 0x03F1, 0x03D6]
|
||||
let index = greekSymbols.firstIndex(of: self.utf32Char)
|
||||
if let pos = index { return UInt32(pos) }
|
||||
return nil
|
||||
}
|
||||
|
||||
var isGreekSymbol : Bool { self.greekSymbolOrder != nil }
|
||||
}
|
||||
58
third-party/SwiftMath/Sources/MathRender/RWLock.swift
vendored
Normal file
58
third-party/SwiftMath/Sources/MathRender/RWLock.swift
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import Foundation
|
||||
|
||||
final class RWLock {
|
||||
init() {
|
||||
pthread_rwlock_init(&lock, nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
pthread_rwlock_destroy(&lock)
|
||||
}
|
||||
|
||||
func read<T>(_ block: () -> T) -> T {
|
||||
pthread_rwlock_rdlock(&lock)
|
||||
defer { pthread_rwlock_unlock(&lock) }
|
||||
return block()
|
||||
}
|
||||
|
||||
func readWrite<T>(_ block: () -> T) -> T {
|
||||
pthread_rwlock_wrlock(&lock)
|
||||
defer { pthread_rwlock_unlock(&lock) }
|
||||
return block()
|
||||
}
|
||||
|
||||
private var lock = pthread_rwlock_t()
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
struct RWLocked<T> {
|
||||
init(wrappedValue: T) {
|
||||
value = wrappedValue
|
||||
}
|
||||
|
||||
var wrappedValue: T {
|
||||
get {
|
||||
lock.read {
|
||||
value
|
||||
}
|
||||
}
|
||||
set {
|
||||
lock.readWrite {
|
||||
value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func readWrite(_ block: (inout T) -> Void) -> (oldValue: T, newValue: T) {
|
||||
lock.readWrite {
|
||||
let old = value
|
||||
block(&value)
|
||||
return (old, value)
|
||||
}
|
||||
}
|
||||
|
||||
private var value: T
|
||||
private let lock = RWLock()
|
||||
}
|
||||
|
||||
1145
third-party/SwiftMath/Sources/MathRender/Tokenization/MTAtomTokenizer.swift
vendored
Normal file
1145
third-party/SwiftMath/Sources/MathRender/Tokenization/MTAtomTokenizer.swift
vendored
Normal file
File diff suppressed because it is too large
Load diff
115
third-party/SwiftMath/Sources/MathRender/Tokenization/MTBreakableElement.swift
vendored
Normal file
115
third-party/SwiftMath/Sources/MathRender/Tokenization/MTBreakableElement.swift
vendored
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
//
|
||||
// MTBreakableElement.swift
|
||||
// SwiftMath
|
||||
//
|
||||
// Created by Claude Code on 2025-12-16.
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
// MARK: - MTElementContent
|
||||
|
||||
/// Represents the content type of a breakable element
|
||||
enum MTElementContent {
|
||||
/// Simple text content
|
||||
case text(String)
|
||||
/// Pre-rendered display (fraction, radical, etc.)
|
||||
case display(MTDisplay)
|
||||
/// Math operator with spacing
|
||||
case `operator`(String, type: MTMathAtomType)
|
||||
/// Explicit spacing
|
||||
case space(CGFloat)
|
||||
/// Superscript or subscript display
|
||||
case script(MTDisplay, isSuper: Bool)
|
||||
}
|
||||
|
||||
// MARK: - MTBreakableElement
|
||||
|
||||
/// Represents a breakable element with pre-calculated width and break rules
|
||||
struct MTBreakableElement {
|
||||
// MARK: Display properties
|
||||
|
||||
/// The content of this element
|
||||
let content: MTElementContent
|
||||
|
||||
/// Pre-calculated width (cached)
|
||||
let width: CGFloat
|
||||
|
||||
/// Height of the element
|
||||
let height: CGFloat
|
||||
|
||||
/// Distance from baseline to top
|
||||
let ascent: CGFloat
|
||||
|
||||
/// Distance from baseline to bottom
|
||||
let descent: CGFloat
|
||||
|
||||
// MARK: Breaking rules
|
||||
|
||||
/// Can break BEFORE this element?
|
||||
let isBreakBefore: Bool
|
||||
|
||||
/// Can break AFTER this element?
|
||||
let isBreakAfter: Bool
|
||||
|
||||
/// Penalty for breaking before (0=good, 100=bad, 150=never)
|
||||
let penaltyBefore: Int
|
||||
|
||||
/// Penalty for breaking after (0=good, 100=bad, 150=never)
|
||||
let penaltyAfter: Int
|
||||
|
||||
// MARK: Relationship tracking
|
||||
|
||||
/// Elements with same groupId must stay together
|
||||
let groupId: UUID?
|
||||
|
||||
/// Parent element ID (for scripts)
|
||||
let parentId: UUID?
|
||||
|
||||
// MARK: Source tracking
|
||||
|
||||
/// Original atom this element was created from
|
||||
let originalAtom: MTMathAtom
|
||||
|
||||
/// Index range in the original math list
|
||||
let indexRange: NSRange
|
||||
|
||||
// MARK: Optional attributes
|
||||
|
||||
/// Text color for this element
|
||||
let color: MTColor?
|
||||
|
||||
/// Background color for this element
|
||||
let backgroundColor: MTColor?
|
||||
|
||||
// MARK: Atomicity flag
|
||||
|
||||
/// If true, NEVER break this element internally
|
||||
let indivisible: Bool
|
||||
}
|
||||
|
||||
// MARK: - Penalty Constants
|
||||
|
||||
/// Penalty values for line breaking decisions
|
||||
enum MTBreakPenalty {
|
||||
/// Best break points (operators, relations)
|
||||
static let best = 0
|
||||
|
||||
/// Good break points (ordinary atoms, after scripts)
|
||||
static let good = 10
|
||||
|
||||
/// Moderate penalty (before fractions, radicals)
|
||||
static let moderate = 15
|
||||
|
||||
/// Acceptable break points
|
||||
static let acceptable = 50
|
||||
|
||||
/// Bad break points (avoid if possible)
|
||||
static let bad = 100
|
||||
|
||||
/// Never break here (grouped elements)
|
||||
static let never = 150
|
||||
}
|
||||
389
third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayGenerator.swift
vendored
Normal file
389
third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayGenerator.swift
vendored
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
//
|
||||
// MTDisplayGenerator.swift
|
||||
// SwiftMath
|
||||
//
|
||||
// Created by Claude Code on 2025-12-16.
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import CoreText
|
||||
|
||||
/// Generates MTDisplay objects from fitted lines of breakable elements
|
||||
class MTDisplayGenerator {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
let font: MTFont
|
||||
let style: MTLineStyle
|
||||
let widthCalculator: MTElementWidthCalculator
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(font: MTFont, style: MTLineStyle) {
|
||||
self.font = font
|
||||
self.style = style
|
||||
self.widthCalculator = MTElementWidthCalculator(font: font, style: style)
|
||||
}
|
||||
|
||||
// MARK: - Display Generation
|
||||
|
||||
/// Generate displays from fitted lines
|
||||
func generateDisplays(from lines: [[MTBreakableElement]], startPosition: CGPoint) -> [MTDisplay] {
|
||||
var allDisplays: [MTDisplay] = []
|
||||
var currentY = startPosition.y
|
||||
|
||||
// Minimum spacing between lines (20% of font size for breathing room)
|
||||
let minimumLineSpacing = font.fontSize * 0.2
|
||||
|
||||
for (index, line) in lines.enumerated() {
|
||||
let (lineDisplays, currentLineMetrics) = generateLine(line, at: CGPoint(x: startPosition.x, y: currentY))
|
||||
allDisplays.append(contentsOf: lineDisplays)
|
||||
|
||||
// Calculate spacing for next line based on actual content heights
|
||||
if index < lines.count - 1 {
|
||||
let nextLine = lines[index + 1]
|
||||
let nextLineAscent = nextLine.map { $0.ascent }.max() ?? 0
|
||||
|
||||
// Space needed = current line's descent + minimum spacing + next line's ascent
|
||||
let spaceNeeded = currentLineMetrics.descent + minimumLineSpacing + nextLineAscent
|
||||
|
||||
// Ensure minimum spacing of 1.2x font size for readability
|
||||
let minSpacing = font.fontSize * 1.2
|
||||
currentY -= max(spaceNeeded, minSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
return allDisplays
|
||||
}
|
||||
|
||||
/// Line metrics for spacing calculation
|
||||
struct LineMetrics {
|
||||
let ascent: CGFloat
|
||||
let descent: CGFloat
|
||||
var height: CGFloat { ascent + descent }
|
||||
}
|
||||
|
||||
/// Generate displays for a single line
|
||||
private func generateLine(_ elements: [MTBreakableElement], at position: CGPoint) -> ([MTDisplay], LineMetrics) {
|
||||
var displays: [MTDisplay] = []
|
||||
var xOffset: CGFloat = 0
|
||||
|
||||
// Calculate line metrics
|
||||
let lineAscent = elements.map { $0.ascent }.max() ?? 0
|
||||
let lineDescent = elements.map { $0.descent }.max() ?? 0
|
||||
|
||||
// Baseline y position
|
||||
let baseline = position.y
|
||||
|
||||
var i = 0
|
||||
while i < elements.count {
|
||||
let element = elements[i]
|
||||
|
||||
// Check if this is part of a group (base + scripts)
|
||||
if let groupId = element.groupId {
|
||||
// Collect all elements in this group
|
||||
var groupElements: [MTBreakableElement] = []
|
||||
var j = i
|
||||
while j < elements.count && elements[j].groupId == groupId {
|
||||
groupElements.append(elements[j])
|
||||
j += 1
|
||||
}
|
||||
|
||||
// Render the group
|
||||
let groupAdvance = renderGroup(groupElements, at: CGPoint(x: position.x + xOffset, y: baseline), displays: &displays)
|
||||
xOffset += groupAdvance
|
||||
i = j
|
||||
} else {
|
||||
// Regular element (not part of a group)
|
||||
|
||||
// CRITICAL: For operators, spacing should be split evenly before and after
|
||||
// The element.width includes both spacing, but we need to position the operator
|
||||
// with half spacing before it
|
||||
var spacingBefore: CGFloat = 0
|
||||
if case .operator(let op, _) = element.content {
|
||||
// Get the actual text width vs element width to calculate spacing
|
||||
let textWidth = widthCalculator.measureText(op)
|
||||
let totalSpacing = element.width - textWidth
|
||||
spacingBefore = totalSpacing / 2
|
||||
}
|
||||
|
||||
let elementPosition = CGPoint(x: position.x + xOffset + spacingBefore, y: baseline)
|
||||
|
||||
switch element.content {
|
||||
case .text(let text):
|
||||
let display = createTextDisplay(text, at: elementPosition, element: element)
|
||||
displays.append(display)
|
||||
|
||||
case .display(let preRenderedDisplay):
|
||||
// Use pre-rendered display (fraction, radical, etc.)
|
||||
let mutableDisplay = preRenderedDisplay
|
||||
mutableDisplay.position = elementPosition
|
||||
displays.append(mutableDisplay)
|
||||
|
||||
case .operator(let op, _):
|
||||
let display = createTextDisplay(op, at: elementPosition, element: element)
|
||||
displays.append(display)
|
||||
|
||||
case .script:
|
||||
// Standalone script (shouldn't happen, but handle gracefully)
|
||||
break
|
||||
|
||||
case .space:
|
||||
// No display for space, just advance position
|
||||
break
|
||||
}
|
||||
|
||||
xOffset += element.width
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
return (displays, LineMetrics(ascent: lineAscent, descent: lineDescent))
|
||||
}
|
||||
|
||||
/// Render a group of elements (base + scripts) and return the horizontal advance
|
||||
private func renderGroup(_ groupElements: [MTBreakableElement], at position: CGPoint, displays: inout [MTDisplay]) -> CGFloat {
|
||||
var baseWidth: CGFloat = 0
|
||||
var superscriptWidth: CGFloat = 0
|
||||
var subscriptWidth: CGFloat = 0
|
||||
var baseXOffset: CGFloat = 0
|
||||
|
||||
// Check if this group has any scripts
|
||||
let hasScripts = groupElements.contains { element in
|
||||
if case .script = element.content {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Track the start index of base displays for dimension adjustment
|
||||
let baseDisplayStartIndex = displays.count
|
||||
|
||||
// First pass: render base elements and collect script widths
|
||||
for element in groupElements {
|
||||
switch element.content {
|
||||
case .script:
|
||||
// Skip scripts in first pass
|
||||
break
|
||||
default:
|
||||
// Render base element
|
||||
let basePosition = CGPoint(x: position.x + baseXOffset, y: position.y)
|
||||
|
||||
switch element.content {
|
||||
case .text(let text):
|
||||
let display = createTextDisplay(text, at: basePosition, element: element, hasScript: hasScripts)
|
||||
displays.append(display)
|
||||
case .display(let preRenderedDisplay):
|
||||
let mutableDisplay = preRenderedDisplay
|
||||
mutableDisplay.position = basePosition
|
||||
displays.append(mutableDisplay)
|
||||
case .operator(let op, _):
|
||||
let display = createTextDisplay(op, at: basePosition, element: element, hasScript: hasScripts)
|
||||
displays.append(display)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
baseWidth += element.width
|
||||
baseXOffset += element.width
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: collect script information for joint positioning
|
||||
var superscriptDisplay: MTDisplay? = nil
|
||||
var subscriptDisplay: MTDisplay? = nil
|
||||
var hasBothScripts = false
|
||||
|
||||
for element in groupElements {
|
||||
if case .script(let scriptDisplay, let isSuper) = element.content {
|
||||
if isSuper {
|
||||
superscriptDisplay = scriptDisplay
|
||||
} else {
|
||||
subscriptDisplay = scriptDisplay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasBothScripts = superscriptDisplay != nil && subscriptDisplay != nil
|
||||
|
||||
// Third pass: render scripts with proper positioning
|
||||
var superScriptShiftUp: CGFloat = 0
|
||||
var subscriptShiftDown: CGFloat = 0
|
||||
|
||||
// Check if base is a glyph (not CTLineDisplay) for special positioning
|
||||
// For glyphs (like large operators), position scripts relative to glyph edges
|
||||
var isGlyphBase = false
|
||||
for disp in displays[baseDisplayStartIndex..<displays.count] {
|
||||
if disp is MTGlyphDisplay {
|
||||
isGlyphBase = true
|
||||
// Get script font metrics for display-based positioning
|
||||
let scriptStyle: MTLineStyle = (style == .display || style == .text) ? .script : .scriptOfScript
|
||||
let scriptFontSize = MTTypesetter.getStyleSize(scriptStyle, font: font)
|
||||
let scriptFont = font.copy(withSize: scriptFontSize)
|
||||
|
||||
if let scriptFontMetrics = scriptFont.mathTable {
|
||||
// Position scripts relative to the glyph's edges (matches MTTypesetter line 571-572)
|
||||
superScriptShiftUp = disp.ascent - scriptFontMetrics.superscriptBaselineDropMax
|
||||
subscriptShiftDown = disp.descent + scriptFontMetrics.subscriptBaselineDropMin
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for element in groupElements {
|
||||
if case .script(let scriptDisplay, let isSuper) = element.content {
|
||||
guard let mathTable = font.mathTable else { continue }
|
||||
|
||||
if !isGlyphBase {
|
||||
// Standard positioning for text (CTLineDisplay)
|
||||
if isSuper {
|
||||
superScriptShiftUp = mathTable.superscriptShiftUp
|
||||
superScriptShiftUp = max(superScriptShiftUp, scriptDisplay.descent + mathTable.superscriptBottomMin)
|
||||
} else {
|
||||
subscriptShiftDown = mathTable.subscriptShiftDown
|
||||
subscriptShiftDown = max(subscriptShiftDown, scriptDisplay.ascent - mathTable.subscriptTopMax)
|
||||
}
|
||||
} else {
|
||||
// For glyphs, apply the minimum constraints (matches MTTypesetter line 594-595, 581-582)
|
||||
if isSuper {
|
||||
superScriptShiftUp = max(superScriptShiftUp, mathTable.superscriptShiftUp)
|
||||
superScriptShiftUp = max(superScriptShiftUp, scriptDisplay.descent + mathTable.superscriptBottomMin)
|
||||
} else {
|
||||
subscriptShiftDown = max(subscriptShiftDown, mathTable.subscriptShiftDown)
|
||||
subscriptShiftDown = max(subscriptShiftDown, scriptDisplay.ascent - mathTable.subscriptTopMax)
|
||||
}
|
||||
}
|
||||
|
||||
if isSuper {
|
||||
superscriptWidth = element.width
|
||||
} else {
|
||||
subscriptWidth = element.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If both scripts present, apply joint positioning adjustments
|
||||
if hasBothScripts, let superDisp = superscriptDisplay, let subDisp = subscriptDisplay,
|
||||
let mathTable = font.mathTable {
|
||||
let subSuperScriptGap = (superScriptShiftUp - superDisp.descent) + (subscriptShiftDown - subDisp.ascent)
|
||||
if subSuperScriptGap < mathTable.subSuperscriptGapMin {
|
||||
// Set the gap to at least the minimum
|
||||
subscriptShiftDown += mathTable.subSuperscriptGapMin - subSuperScriptGap
|
||||
let superscriptBottomDelta = mathTable.superscriptBottomMaxWithSubscript - (superScriptShiftUp - superDisp.descent)
|
||||
if superscriptBottomDelta > 0 {
|
||||
// Superscript is lower than the max allowed by the font with a subscript
|
||||
superScriptShiftUp += superscriptBottomDelta
|
||||
subscriptShiftDown -= superscriptBottomDelta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate italic correction (delta) for superscript positioning
|
||||
// Superscripts are positioned at baseWidth + delta, subscripts at baseWidth
|
||||
var delta: CGFloat = 0
|
||||
if superscriptDisplay != nil {
|
||||
// Get italic correction from the base display if it's a glyph
|
||||
for disp in displays[baseDisplayStartIndex..<displays.count] {
|
||||
if let glyphDisplay = disp as? MTGlyphDisplay,
|
||||
let mathTable = font.mathTable {
|
||||
delta = mathTable.getItalicCorrection(glyphDisplay.glyph)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fourth pass: create wrapped script displays with final positions
|
||||
for element in groupElements {
|
||||
if case .script(let scriptDisplay, let isSuper) = element.content {
|
||||
let scriptShift = isSuper ? superScriptShiftUp : -subscriptShiftDown
|
||||
let scriptType: MTMathListDisplay.LinePosition = isSuper ? .superscript : .ssubscript
|
||||
// Superscripts get delta added, subscripts don't (matches MTTypesetter line 622, 624)
|
||||
let deltaOffset = isSuper ? delta : 0
|
||||
let scriptPosition = CGPoint(x: position.x + baseWidth + deltaOffset, y: position.y + scriptShift)
|
||||
|
||||
// Reset the scriptDisplay's position to (0, 0) since it will be positioned by the wrapper
|
||||
let mutableScript = scriptDisplay
|
||||
mutableScript.position = CGPoint.zero
|
||||
|
||||
let wrappedScript = MTMathListDisplay(
|
||||
withDisplays: [mutableScript],
|
||||
range: scriptDisplay.range
|
||||
)
|
||||
wrappedScript.type = scriptType
|
||||
wrappedScript.position = scriptPosition
|
||||
wrappedScript.index = 0 // Index of the base atom this script belongs to
|
||||
|
||||
displays.append(wrappedScript)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate horizontal advance: base width + max(script widths) + spacing
|
||||
// This matches MTTypesetter.makeScripts() logic (line 626)
|
||||
// Superscript width includes delta, subscript doesn't
|
||||
let scriptWidth = max(superscriptWidth + delta, subscriptWidth)
|
||||
let spaceAfterScript = font.mathTable?.spaceAfterScript ?? 0
|
||||
let totalWidth = baseWidth + scriptWidth + spaceAfterScript
|
||||
|
||||
// If this group has scripts, adjust the base display's dimensions to include scripts
|
||||
// This matches the legacy typesetter behavior where display.ascent == line.ascent
|
||||
if hasScripts && displays.count > baseDisplayStartIndex {
|
||||
// Calculate the full extent of the group including scripts
|
||||
var maxAscent: CGFloat = 0
|
||||
var maxDescent: CGFloat = 0
|
||||
|
||||
for i in baseDisplayStartIndex..<displays.count {
|
||||
let disp = displays[i]
|
||||
let topY = disp.position.y + disp.ascent
|
||||
let bottomY = disp.position.y - disp.descent
|
||||
|
||||
maxAscent = max(maxAscent, topY - position.y)
|
||||
maxDescent = max(maxDescent, position.y - bottomY)
|
||||
}
|
||||
|
||||
// Update the first base display's dimensions to reflect the full extent
|
||||
// Width should be base + script, without the spaceAfterScript (that's for cursor advancement)
|
||||
let baseDisplay = displays[baseDisplayStartIndex]
|
||||
baseDisplay.ascent = maxAscent
|
||||
baseDisplay.descent = maxDescent
|
||||
baseDisplay.width = baseWidth + scriptWidth
|
||||
displays[baseDisplayStartIndex] = baseDisplay
|
||||
}
|
||||
|
||||
return totalWidth
|
||||
}
|
||||
|
||||
/// Create a text display for a character or string
|
||||
private func createTextDisplay(_ text: String, at position: CGPoint, element: MTBreakableElement, hasScript: Bool = false) -> MTDisplay {
|
||||
let attrString = NSMutableAttributedString(string: text)
|
||||
attrString.addAttribute(
|
||||
NSAttributedString.Key(kCTFontAttributeName as String),
|
||||
value: font.ctFont as Any,
|
||||
range: NSMakeRange(0, attrString.length)
|
||||
)
|
||||
|
||||
// If the atom was fused (multiple ordinary chars combined), use fusedAtoms
|
||||
// Otherwise, use the original atom
|
||||
let atoms: [MTMathAtom]
|
||||
if !element.originalAtom.fusedAtoms.isEmpty {
|
||||
atoms = element.originalAtom.fusedAtoms
|
||||
} else {
|
||||
atoms = [element.originalAtom]
|
||||
}
|
||||
|
||||
let display = MTCTLineDisplay(
|
||||
withString: attrString,
|
||||
position: position,
|
||||
range: element.indexRange,
|
||||
font: font,
|
||||
atoms: atoms
|
||||
)
|
||||
|
||||
// Mark if this base element has associated scripts
|
||||
display.hasScript = hasScript
|
||||
|
||||
return display
|
||||
}
|
||||
}
|
||||
88
third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayPreRenderer.swift
vendored
Normal file
88
third-party/SwiftMath/Sources/MathRender/Tokenization/MTDisplayPreRenderer.swift
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
// MTDisplayPreRenderer.swift
|
||||
// SwiftMath
|
||||
//
|
||||
// Created by Claude Code on 2025-12-16.
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
/// Pre-renders complex atoms (fractions, radicals, etc.) as MTDisplay objects during tokenization
|
||||
class MTDisplayPreRenderer {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
let font: MTFont
|
||||
let style: MTLineStyle
|
||||
let cramped: Bool
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(font: MTFont, style: MTLineStyle, cramped: Bool) {
|
||||
self.font = font
|
||||
self.style = style
|
||||
self.cramped = cramped
|
||||
}
|
||||
|
||||
// MARK: - Script Rendering
|
||||
|
||||
/// Render a script (superscript or subscript) as a display
|
||||
func renderScript(_ mathList: MTMathList, isSuper: Bool) -> MTDisplay? {
|
||||
let scriptStyle = getScriptStyle()
|
||||
let scriptCramped = isSuper ? cramped : true // Subscripts are always cramped
|
||||
|
||||
// Scale the font for the script style
|
||||
let scriptFontSize = MTTypesetter.getStyleSize(scriptStyle, font: font)
|
||||
let scriptFont = font.copy(withSize: scriptFontSize)
|
||||
|
||||
guard let display = MTTypesetter.createLineForMathList(
|
||||
mathList,
|
||||
font: scriptFont,
|
||||
style: scriptStyle,
|
||||
cramped: scriptCramped,
|
||||
spaced: false
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the result is a MTMathListDisplay with a single subdisplay, unwrap it
|
||||
// This matches the behavior of the legacy typesetter
|
||||
if display.subDisplays.count == 1 {
|
||||
return display.subDisplays[0]
|
||||
}
|
||||
|
||||
return display
|
||||
}
|
||||
|
||||
/// Get the appropriate style for scripts
|
||||
private func getScriptStyle() -> MTLineStyle {
|
||||
switch style {
|
||||
case .display, .text:
|
||||
return .script
|
||||
case .script, .scriptOfScript:
|
||||
return .scriptOfScript
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
/// Pre-render a simple math list without width constraints
|
||||
/// Used for rendering content inside fractions, radicals, etc.
|
||||
func renderMathList(_ mathList: MTMathList?, style renderStyle: MTLineStyle? = nil, cramped renderCramped: Bool? = nil) -> MTDisplay? {
|
||||
guard let mathList = mathList else { return nil }
|
||||
|
||||
let actualStyle = renderStyle ?? style
|
||||
let actualCramped = renderCramped ?? cramped
|
||||
|
||||
return MTTypesetter.createLineForMathList(
|
||||
mathList,
|
||||
font: font,
|
||||
style: actualStyle,
|
||||
cramped: actualCramped,
|
||||
spaced: false
|
||||
)
|
||||
}
|
||||
}
|
||||
165
third-party/SwiftMath/Sources/MathRender/Tokenization/MTElementWidthCalculator.swift
vendored
Normal file
165
third-party/SwiftMath/Sources/MathRender/Tokenization/MTElementWidthCalculator.swift
vendored
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
//
|
||||
// MTElementWidthCalculator.swift
|
||||
// SwiftMath
|
||||
//
|
||||
// Created by Claude Code on 2025-12-16.
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreText
|
||||
import CoreGraphics
|
||||
|
||||
/// Calculates widths for breakable elements with appropriate spacing
|
||||
class MTElementWidthCalculator {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
let font: MTFont
|
||||
let style: MTLineStyle
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(font: MTFont, style: MTLineStyle) {
|
||||
self.font = font
|
||||
self.style = style
|
||||
}
|
||||
|
||||
// MARK: - Text Width Measurement
|
||||
|
||||
/// Measure width of simple text
|
||||
func measureText(_ text: String) -> CGFloat {
|
||||
guard !text.isEmpty else { return 0 }
|
||||
|
||||
let attrString = NSAttributedString(string: text, attributes: [
|
||||
kCTFontAttributeName as NSAttributedString.Key: font.ctFont as Any
|
||||
])
|
||||
let line = CTLineCreateWithAttributedString(attrString as CFAttributedString)
|
||||
return CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil))
|
||||
}
|
||||
|
||||
// MARK: - Operator Width Measurement
|
||||
|
||||
/// Measure width of operator with appropriate spacing
|
||||
func measureOperator(_ op: String, type: MTMathAtomType) -> CGFloat {
|
||||
let baseWidth = measureText(op)
|
||||
let spacing = getOperatorSpacing(type)
|
||||
return baseWidth + spacing
|
||||
}
|
||||
|
||||
/// Get spacing for an operator (both sides)
|
||||
private func getOperatorSpacing(_ type: MTMathAtomType) -> CGFloat {
|
||||
guard let mathTable = font.mathTable else { return 0 }
|
||||
let muUnit = mathTable.muUnit
|
||||
|
||||
switch type {
|
||||
case .binaryOperator:
|
||||
// Binary operators: 4mu on each side = 8mu total
|
||||
return 2 * muUnit * 4
|
||||
|
||||
case .relation:
|
||||
// Relations: 5mu on each side = 10mu total
|
||||
return 2 * muUnit * 5
|
||||
|
||||
case .largeOperator:
|
||||
// Large operators in inline mode: 1mu on each side
|
||||
if style == .display || style == .text {
|
||||
return 0 // In display mode, handled by MTLargeOpLimitsDisplay
|
||||
}
|
||||
return 2 * muUnit * 1
|
||||
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Display Width Measurement
|
||||
|
||||
/// Measure width of a pre-rendered display
|
||||
func measureDisplay(_ display: MTDisplay) -> CGFloat {
|
||||
return display.width
|
||||
}
|
||||
|
||||
// MARK: - Space Width Measurement
|
||||
|
||||
/// Get width of explicit spacing command
|
||||
func measureSpace(_ spaceType: MTMathAtomType) -> CGFloat {
|
||||
guard let mathTable = font.mathTable else { return 0 }
|
||||
let muUnit = mathTable.muUnit
|
||||
|
||||
// Note: These are the explicit spacing commands in LaTeX
|
||||
// \, = thin space (3mu)
|
||||
// \: = medium space (4mu)
|
||||
// \; = thick space (5mu)
|
||||
// \quad = 1em
|
||||
// \qquad = 2em
|
||||
|
||||
switch spaceType {
|
||||
case .space:
|
||||
// Default space - context dependent
|
||||
// For now, use thin space
|
||||
return muUnit * 3
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Measure explicit space value
|
||||
func measureExplicitSpace(_ width: CGFloat) -> CGFloat {
|
||||
return width
|
||||
}
|
||||
|
||||
// MARK: - Inter-element Spacing
|
||||
|
||||
/// Get inter-element spacing between two atom types
|
||||
func getInterElementSpacing(left: MTMathAtomType, right: MTMathAtomType) -> CGFloat {
|
||||
let leftIndex = getInterElementSpaceArrayIndexForType(left, row: true)
|
||||
let rightIndex = getInterElementSpaceArrayIndexForType(right, row: false)
|
||||
let spaceArray = getInterElementSpaces()[Int(leftIndex)]
|
||||
let spaceType = spaceArray[Int(rightIndex)]
|
||||
|
||||
guard spaceType != .invalid else {
|
||||
// Should not happen in well-formed math
|
||||
return 0
|
||||
}
|
||||
|
||||
let spaceMultiplier = getSpacingInMu(spaceType)
|
||||
if spaceMultiplier > 0, let mathTable = font.mathTable {
|
||||
return CGFloat(spaceMultiplier) * mathTable.muUnit
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/// Get spacing multiplier in mu units
|
||||
private func getSpacingInMu(_ spaceType: InterElementSpaceType) -> Int {
|
||||
switch style {
|
||||
case .display, .text:
|
||||
switch spaceType {
|
||||
case .none, .invalid:
|
||||
return 0
|
||||
case .thin:
|
||||
return 3
|
||||
case .nsThin, .nsMedium, .nsThick:
|
||||
// ns = non-script, same as regular in display/text mode
|
||||
switch spaceType {
|
||||
case .nsThin: return 3
|
||||
case .nsMedium: return 4
|
||||
case .nsThick: return 5
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
case .script, .scriptOfScript:
|
||||
switch spaceType {
|
||||
case .none, .invalid:
|
||||
return 0
|
||||
case .thin:
|
||||
return 3
|
||||
case .nsThin, .nsMedium, .nsThick:
|
||||
// In script mode, ns types don't add space
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
266
third-party/SwiftMath/Sources/MathRender/Tokenization/MTLineFitter.swift
vendored
Normal file
266
third-party/SwiftMath/Sources/MathRender/Tokenization/MTLineFitter.swift
vendored
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
//
|
||||
// MTLineFitter.swift
|
||||
// SwiftMath
|
||||
//
|
||||
// Created by Claude Code on 2025-12-16.
|
||||
// This software may be modified and distributed under the terms of the
|
||||
// MIT license. See the LICENSE file for details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
/// Fits breakable elements into lines respecting width constraints and break rules
|
||||
class MTLineFitter {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
let maxWidth: CGFloat
|
||||
let margin: CGFloat
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(maxWidth: CGFloat, margin: CGFloat = 0) {
|
||||
self.maxWidth = maxWidth
|
||||
self.margin = margin
|
||||
}
|
||||
|
||||
// MARK: - Line Fitting
|
||||
|
||||
/// Fit elements into lines using greedy algorithm with backtracking
|
||||
func fitLines(_ elements: [MTBreakableElement]) -> [[MTBreakableElement]] {
|
||||
guard !elements.isEmpty else { return [] }
|
||||
guard maxWidth > 0 else { return [elements] } // No width constraint
|
||||
|
||||
let debugPunctuation = !"".isEmpty // Enable to debug line breaking issues
|
||||
|
||||
if debugPunctuation {
|
||||
print("\n=== MTLineFitter: fitting \(elements.count) elements, maxWidth=\(maxWidth) ===")
|
||||
for (idx, elem) in elements.enumerated() {
|
||||
if case .text(let t) = elem.content {
|
||||
print("[\(idx)] '\(t)' breakBefore=\(elem.isBreakBefore) breakAfter=\(elem.isBreakAfter) width=\(elem.width)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lines: [[MTBreakableElement]] = [[]]
|
||||
var currentWidth: CGFloat = 0
|
||||
var i = 0
|
||||
|
||||
while i < elements.count {
|
||||
let element = elements[i]
|
||||
|
||||
if debugPunctuation, case .text(let t) = element.content {
|
||||
print("\n Processing element[\(i)]: '\(t)' breakBefore=\(element.isBreakBefore)")
|
||||
}
|
||||
|
||||
// Handle grouped elements (base + scripts)
|
||||
if let groupId = element.groupId {
|
||||
let (groupElements, nextIndex) = collectGroup(elements, startIndex: i, groupId: groupId)
|
||||
|
||||
// Calculate group width correctly for scripts
|
||||
// Scripts overlap vertically, so width = max(script widths), not sum
|
||||
let groupWidth = calculateGroupWidth(groupElements)
|
||||
|
||||
// Check if group fits on current line
|
||||
if !lines.last!.isEmpty && currentWidth + groupWidth > maxWidth - margin {
|
||||
// Group doesn't fit - check if first element of group can start a new line
|
||||
if groupElements.first?.isBreakBefore ?? true {
|
||||
// Can start new line
|
||||
lines.append([])
|
||||
currentWidth = 0
|
||||
} else {
|
||||
// Cannot start new line - keep with previous line (allow overflow)
|
||||
// This handles cases like punctuation after base+script groups
|
||||
}
|
||||
}
|
||||
|
||||
// Add entire group to current line
|
||||
lines[lines.count - 1].append(contentsOf: groupElements)
|
||||
currentWidth += groupWidth
|
||||
i = nextIndex
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if element fits on current line
|
||||
if !lines.last!.isEmpty && currentWidth + element.width > maxWidth - margin {
|
||||
if debugPunctuation, case .text = element.content {
|
||||
print(" Doesn't fit (width=\(currentWidth) + \(element.width) > \(maxWidth)), current line has \(lines.last!.count) elements")
|
||||
}
|
||||
// Element doesn't fit - find best break point in current line
|
||||
if let breakIndex = findBestBreak(in: lines[lines.count - 1]) {
|
||||
if debugPunctuation {
|
||||
print(" Found break at index \(breakIndex) out of \(lines.last!.count) elements")
|
||||
if breakIndex < lines.last!.count {
|
||||
if case .text(let t) = lines.last![breakIndex].content {
|
||||
print(" Break at element: '\(t)'")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Found a break point - move elements from breakIndex onward to next line
|
||||
let moveElements = Array(lines[lines.count - 1][breakIndex...])
|
||||
let oldLine = Array(lines[lines.count - 1][..<breakIndex])
|
||||
|
||||
// Verify the first element being moved can start a line
|
||||
// (findBestBreak already ensures this, but double-check)
|
||||
if moveElements.first?.isBreakBefore ?? true {
|
||||
// Move elements to new line
|
||||
lines[lines.count - 1] = oldLine
|
||||
lines.append(moveElements)
|
||||
currentWidth = moveElements.reduce(0) { $0 + $1.width }
|
||||
|
||||
// Now check if current element should go on new line or stay with old line
|
||||
if debugPunctuation, case .text(let t) = element.content {
|
||||
print(" Checking element '\(t)' (i=\(i)): breakBefore=\(element.isBreakBefore)")
|
||||
}
|
||||
if !element.isBreakBefore {
|
||||
// CRITICAL FIX: Current element cannot start a line, so it's part of the
|
||||
// unbreakable sequence that was just moved to the new line.
|
||||
// Add it to the NEW line, not the old line!
|
||||
// Example: "matrices" breaks before 'm', so 'm','a','t','r','i' move to new line.
|
||||
// When we process 'c', it can't start a line, so it must join the new line.
|
||||
if debugPunctuation, case .text(let t) = element.content {
|
||||
print(" -> Adding '\(t)' to new line (part of unbreakable sequence)")
|
||||
}
|
||||
lines[lines.count - 1].append(element)
|
||||
currentWidth += element.width
|
||||
i += 1
|
||||
continue
|
||||
} else {
|
||||
if debugPunctuation, case .text(let t) = element.content {
|
||||
print(" -> Adding '\(t)' to new line (can start line)")
|
||||
}
|
||||
}
|
||||
// Current element can start a line, will be added to new line below
|
||||
} else {
|
||||
// Should not happen if findBestBreak is correct, but handle gracefully
|
||||
// Keep elements on current line (allow overflow)
|
||||
lines[lines.count - 1].append(contentsOf: moveElements)
|
||||
currentWidth += moveElements.reduce(0) { $0 + $1.width }
|
||||
}
|
||||
} else {
|
||||
// No good break point found
|
||||
// Check if current element can start a new line
|
||||
if element.isBreakBefore {
|
||||
// Element can start new line
|
||||
lines.append([])
|
||||
currentWidth = 0
|
||||
} else {
|
||||
// Element cannot start a new line (e.g., closing punctuation)
|
||||
// Keep it on current line even if it causes overflow
|
||||
// This respects punctuation rules over width constraints
|
||||
lines[lines.count - 1].append(element)
|
||||
currentWidth += element.width
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add element to current line (may overflow if indivisible and too wide)
|
||||
lines[lines.count - 1].append(element)
|
||||
currentWidth += element.width
|
||||
i += 1
|
||||
}
|
||||
|
||||
let finalLines = lines.filter { !$0.isEmpty }
|
||||
|
||||
if debugPunctuation {
|
||||
print("\n=== Final lines: ===")
|
||||
for (lineIdx, line) in finalLines.enumerated() {
|
||||
print("Line \(lineIdx):")
|
||||
for elem in line {
|
||||
if case .text(let t) = elem.content {
|
||||
print(" '\(t)'", terminator: "")
|
||||
}
|
||||
}
|
||||
print()
|
||||
}
|
||||
}
|
||||
|
||||
return finalLines
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
/// Calculate the correct width for a group of elements (e.g., base + scripts)
|
||||
/// Scripts overlap vertically, so the group width is not the sum of all widths
|
||||
private func calculateGroupWidth(_ groupElements: [MTBreakableElement]) -> CGFloat {
|
||||
// For grouped elements (base + scripts), just sum all widths
|
||||
// The display generator will handle the actual positioning and overlap
|
||||
// This is just for line fitting purposes
|
||||
return groupElements.reduce(0) { $0 + $1.width }
|
||||
}
|
||||
|
||||
/// Collect all elements that share the same groupId
|
||||
private func collectGroup(_ elements: [MTBreakableElement], startIndex: Int, groupId: UUID) -> ([MTBreakableElement], Int) {
|
||||
var groupElements: [MTBreakableElement] = []
|
||||
var index = startIndex
|
||||
|
||||
while index < elements.count && elements[index].groupId == groupId {
|
||||
groupElements.append(elements[index])
|
||||
index += 1
|
||||
}
|
||||
|
||||
return (groupElements, index)
|
||||
}
|
||||
|
||||
/// Find the best break point in a line
|
||||
/// Returns the index where the break should occur (elements from this index move to next line)
|
||||
private func findBestBreak(in line: [MTBreakableElement]) -> Int? {
|
||||
var bestIndex: Int? = nil
|
||||
var lowestPenalty = Int.max
|
||||
|
||||
let debugBreak = !"".isEmpty // Enable to debug break point selection
|
||||
|
||||
// Scan from right to left to prefer breaking later in the line
|
||||
// Note: Skip the last element (idx == line.count - 1) because breaking after it
|
||||
// would move 0 elements to the next line, which is pointless
|
||||
for (idx, element) in line.enumerated().reversed() {
|
||||
// Skip the last element - we need to move at least 1 element to the next line
|
||||
if idx >= line.count - 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Can we break after this element?
|
||||
let canBreakAfter = element.isBreakAfter
|
||||
let penaltyAfter = element.penaltyAfter
|
||||
|
||||
// Check if next element (which would move to new line) allows breaking before it
|
||||
let canBreakBeforeNext = line[idx + 1].isBreakBefore
|
||||
let penaltyBeforeNext = line[idx + 1].penaltyBefore
|
||||
|
||||
// We can break here only if BOTH:
|
||||
// 1. Current element allows breaking after it
|
||||
// 2. Next element allows breaking before it
|
||||
if canBreakAfter && canBreakBeforeNext {
|
||||
let totalPenalty = max(penaltyAfter, penaltyBeforeNext)
|
||||
if totalPenalty < lowestPenalty {
|
||||
if debugBreak && idx < line.count - 1 {
|
||||
let currText = if case .text(let t) = element.content { t } else { "?" }
|
||||
let nextText = if case .text(let t) = line[idx + 1].content { t } else { "?" }
|
||||
print(" Considering break: '\(currText)' | '\(nextText)' at idx=\(idx), penalty=\(totalPenalty)")
|
||||
}
|
||||
bestIndex = idx + 1
|
||||
lowestPenalty = totalPenalty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if debugBreak {
|
||||
print(" Best break: index=\(bestIndex ?? -1), penalty=\(lowestPenalty)")
|
||||
}
|
||||
|
||||
// Only return if we found an acceptable break point
|
||||
if let index = bestIndex, lowestPenalty <= MTBreakPenalty.bad {
|
||||
return index
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Check if a line width exceeds the maximum
|
||||
private func exceedsMaxWidth(_ width: CGFloat) -> Bool {
|
||||
return width > maxWidth - margin
|
||||
}
|
||||
}
|
||||
19
third-party/SwiftSVG/BUILD
vendored
Normal file
19
third-party/SwiftSVG/BUILD
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SwiftSVG",
|
||||
module_name = "SwiftSVG",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//third-party/XMLCoder",
|
||||
"//third-party/Swift2D",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
93
third-party/SwiftSVG/Sources/Circle.swift
vendored
Normal file
93
third-party/SwiftSVG/Sources/Circle.swift
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import Swift2D
|
||||
import XMLCoder
|
||||
|
||||
/// Basic shape, used to draw circles based on a center point and a radius.
|
||||
///
|
||||
/// The arc of a ‘circle’ element begins at the "3 o'clock" point on the radius and progresses towards the
|
||||
/// "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform
|
||||
/// in the same manner as the geometry of the element.
|
||||
///
|
||||
/// ## Documentation
|
||||
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle)
|
||||
/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#CircleElement)
|
||||
public struct Circle: Element {
|
||||
|
||||
/// The x-axis coordinate of the center of the circle.
|
||||
public var x: Double = 0.0
|
||||
/// The y-axis coordinate of the center of the circle.
|
||||
public var y: Double = 0.0
|
||||
/// The radius of the circle.
|
||||
public var r: Double = 0.0
|
||||
|
||||
// MARK: CoreAttributes
|
||||
|
||||
public var id: String?
|
||||
|
||||
// MARK: PresentationAttributes
|
||||
|
||||
public var fillColor: String?
|
||||
public var fillOpacity: Double?
|
||||
public var fillRule: Fill.Rule?
|
||||
public var strokeColor: String?
|
||||
public var strokeWidth: Double?
|
||||
public var strokeOpacity: Double?
|
||||
public var strokeLineCap: Stroke.LineCap?
|
||||
public var strokeLineJoin: Stroke.LineJoin?
|
||||
public var strokeMiterLimit: Double?
|
||||
public var transform: String?
|
||||
|
||||
// MARK: StylingAttributes
|
||||
|
||||
public var style: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case x = "cx"
|
||||
case y = "cy"
|
||||
case r
|
||||
case id
|
||||
case fillColor = "fill"
|
||||
case fillOpacity = "fill-opacity"
|
||||
case fillRule = "fill-rule"
|
||||
case strokeColor = "stroke"
|
||||
case strokeWidth = "stroke-width"
|
||||
case strokeOpacity = "stroke-opacity"
|
||||
case strokeLineCap = "stroke-linecap"
|
||||
case strokeLineJoin = "stroke-linejoin"
|
||||
case strokeMiterLimit = "stroke-miterlimit"
|
||||
case transform
|
||||
case style
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(x: Double, y: Double, r: Double) {
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.r = r
|
||||
}
|
||||
}
|
||||
|
||||
extension Circle: CustomStringConvertible {
|
||||
public var description: String {
|
||||
let desc = "<circle cx=\"\(x)\" cy=\"\(y)\" r=\"\(r)\""
|
||||
return desc + " \(attributeDescription) />"
|
||||
}
|
||||
}
|
||||
|
||||
extension Circle: DirectionalCommandRepresentable {
|
||||
public func commands(clockwise: Bool) throws -> [Path.Command] {
|
||||
EllipseProcessor(circle: self).commands(clockwise: clockwise)
|
||||
}
|
||||
}
|
||||
|
||||
extension Circle: DynamicNodeDecoding {
|
||||
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
|
||||
extension Circle: DynamicNodeEncoding {
|
||||
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
15
third-party/SwiftSVG/Sources/CommandRepresentable.swift
vendored
Normal file
15
third-party/SwiftSVG/Sources/CommandRepresentable.swift
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/// Elements conforming to `CommandRepresentable` can be expressed in the form of `Path.Command`s.
|
||||
public protocol CommandRepresentable {
|
||||
func commands() throws -> [Path.Command]
|
||||
}
|
||||
|
||||
public protocol DirectionalCommandRepresentable: CommandRepresentable {
|
||||
func commands(clockwise: Bool) throws -> [Path.Command]
|
||||
}
|
||||
|
||||
public extension DirectionalCommandRepresentable {
|
||||
/// Defaults to anti/counter-clockwise commands.
|
||||
func commands() throws -> [Path.Command] {
|
||||
try commands(clockwise: false)
|
||||
}
|
||||
}
|
||||
58
third-party/SwiftSVG/Sources/Container.swift
vendored
Normal file
58
third-party/SwiftSVG/Sources/Container.swift
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
public protocol Container {
|
||||
var circles: [Circle]? { get set }
|
||||
var ellipses: [Ellipse]? { get set }
|
||||
var groups: [Group]? { get set }
|
||||
var lines: [Line]? { get set }
|
||||
var paths: [Path]? { get set }
|
||||
var polygons: [Polygon]? { get set }
|
||||
var polylines: [Polyline]? { get set }
|
||||
var rectangles: [Rectangle]? { get set }
|
||||
var texts: [Text]? { get set }
|
||||
}
|
||||
|
||||
enum ContainerKeys: String, CodingKey {
|
||||
case circles = "circle"
|
||||
case ellipses = "ellipse"
|
||||
case groups = "g"
|
||||
case lines = "line"
|
||||
case paths = "path"
|
||||
case polylines = "polyline"
|
||||
case polygons = "polygon"
|
||||
case rectangles = "rect"
|
||||
case texts = "text"
|
||||
}
|
||||
|
||||
public extension Container {
|
||||
var containerDescription: String {
|
||||
var contents: String = ""
|
||||
|
||||
let circles = circles?.compactMap(\.description) ?? []
|
||||
circles.forEach { contents.append("\n\($0)") }
|
||||
|
||||
let ellipses = ellipses?.compactMap(\.description) ?? []
|
||||
ellipses.forEach { contents.append("\n\($0)") }
|
||||
|
||||
let groups = groups?.compactMap(\.description) ?? []
|
||||
groups.forEach { contents.append("\n\($0)") }
|
||||
|
||||
let lines = lines?.compactMap(\.description) ?? []
|
||||
lines.forEach { contents.append("\n\($0)") }
|
||||
|
||||
let paths = paths?.compactMap(\.description) ?? []
|
||||
paths.forEach { contents.append("\n\($0)") }
|
||||
|
||||
let polylines = polylines?.compactMap(\.description) ?? []
|
||||
polylines.forEach { contents.append("\n\($0)") }
|
||||
|
||||
let polygons = polygons?.compactMap(\.description) ?? []
|
||||
polygons.forEach { contents.append("\n\($0)") }
|
||||
|
||||
let rectangles = rectangles?.compactMap(\.description) ?? []
|
||||
rectangles.forEach { contents.append("\n\($0)") }
|
||||
|
||||
let texts = texts?.compactMap(\.description) ?? []
|
||||
texts.forEach { contents.append("\n\($0)") }
|
||||
|
||||
return contents
|
||||
}
|
||||
}
|
||||
17
third-party/SwiftSVG/Sources/CoreAttributes.swift
vendored
Normal file
17
third-party/SwiftSVG/Sources/CoreAttributes.swift
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
public protocol CoreAttributes {
|
||||
var id: String? { get set }
|
||||
}
|
||||
|
||||
enum CoreAttributesKeys: String, CodingKey {
|
||||
case id
|
||||
}
|
||||
|
||||
public extension CoreAttributes {
|
||||
var coreDescription: String {
|
||||
if let id {
|
||||
"\(CoreAttributesKeys.id.rawValue)=\"\(id)\""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
46
third-party/SwiftSVG/Sources/Element.swift
vendored
Normal file
46
third-party/SwiftSVG/Sources/Element.swift
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
public protocol Element: CoreAttributes, PresentationAttributes, StylingAttributes {}
|
||||
|
||||
public extension Element {
|
||||
var attributeDescription: String {
|
||||
var components: [String] = []
|
||||
|
||||
if !coreDescription.isEmpty {
|
||||
components.append(coreDescription)
|
||||
}
|
||||
if !presentationDescription.isEmpty {
|
||||
components.append(presentationDescription)
|
||||
}
|
||||
if !stylingDescription.isEmpty {
|
||||
components.append(stylingDescription)
|
||||
}
|
||||
|
||||
return components.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
public extension CommandRepresentable where Self: Element {
|
||||
/// When a `Path` is accessed on an element, the path that is returned should have the supplied transformations
|
||||
/// applied.
|
||||
///
|
||||
/// For instance, if
|
||||
/// * a `Path.data` contains relative elements,
|
||||
/// * and `transformations` contains a `.translate`
|
||||
///
|
||||
/// Than the path created will not only use 'absolute' instructions, but those instructions will be modified to
|
||||
/// include the required transformation.
|
||||
func path(applying transformations: [Transformation] = []) throws -> Path {
|
||||
var _transformations = transformations
|
||||
_transformations.append(contentsOf: self.transformations)
|
||||
|
||||
let commands = try commands().map { $0.applying(transformations: _transformations) }
|
||||
|
||||
var path = Path(commands: commands)
|
||||
path.fillColor = fillColor
|
||||
path.fillOpacity = fillOpacity
|
||||
path.strokeColor = strokeColor
|
||||
path.strokeOpacity = strokeOpacity
|
||||
path.strokeWidth = strokeWidth
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
93
third-party/SwiftSVG/Sources/Ellipse.swift
vendored
Normal file
93
third-party/SwiftSVG/Sources/Ellipse.swift
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import Swift2D
|
||||
import XMLCoder
|
||||
|
||||
/// SVG basic shape, used to create ellipses based on a center coordinate, and both their x and y radius.
|
||||
///
|
||||
/// The arc of an ‘ellipse’ element begins at the "3 o'clock" point on the radius and progresses towards the
|
||||
/// "9 o'clock" point. The starting point and direction of the arc are affected by the user space transform in the same
|
||||
/// manner as the geometry of the element.
|
||||
public struct Ellipse: Element {
|
||||
|
||||
/// The x position of the ellipse.
|
||||
public var x: Double = 0.0
|
||||
/// The y position of the ellipse.
|
||||
public var y: Double = 0.0
|
||||
/// The radius of the ellipse on the x axis.
|
||||
public var rx: Double = 0.0
|
||||
/// The radius of the ellipse on the y axis.
|
||||
public var ry: Double = 0.0
|
||||
|
||||
// MARK: CoreAttributes
|
||||
|
||||
public var id: String?
|
||||
|
||||
// MARK: PresentationAttributes
|
||||
|
||||
public var fillColor: String?
|
||||
public var fillOpacity: Double?
|
||||
public var fillRule: Fill.Rule?
|
||||
public var strokeColor: String?
|
||||
public var strokeWidth: Double?
|
||||
public var strokeOpacity: Double?
|
||||
public var strokeLineCap: Stroke.LineCap?
|
||||
public var strokeLineJoin: Stroke.LineJoin?
|
||||
public var strokeMiterLimit: Double?
|
||||
public var transform: String?
|
||||
|
||||
// MARK: StylingAttributes
|
||||
|
||||
public var style: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case x = "cx"
|
||||
case y = "cy"
|
||||
case rx
|
||||
case ry
|
||||
case id
|
||||
case fillColor = "fill"
|
||||
case fillOpacity = "fill-opacity"
|
||||
case fillRule = "fill-rule"
|
||||
case strokeColor = "stroke"
|
||||
case strokeWidth = "stroke-width"
|
||||
case strokeOpacity = "stroke-opacity"
|
||||
case strokeLineCap = "stroke-linecap"
|
||||
case strokeLineJoin = "stroke-linejoin"
|
||||
case strokeMiterLimit = "stroke-miterlimit"
|
||||
case transform
|
||||
case style
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(x: Double, y: Double, rx: Double, ry: Double) {
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.rx = rx
|
||||
self.ry = ry
|
||||
}
|
||||
}
|
||||
|
||||
extension Ellipse: CustomStringConvertible {
|
||||
public var description: String {
|
||||
let desc = "<ellipse cx=\"\(x)\" cy=\"\(y)\" rx=\"\(rx)\" ry=\"\(ry)\""
|
||||
return desc + " \(attributeDescription) />"
|
||||
}
|
||||
}
|
||||
|
||||
extension Ellipse: DirectionalCommandRepresentable {
|
||||
public func commands(clockwise: Bool) throws -> [Path.Command] {
|
||||
EllipseProcessor(ellipse: self).commands(clockwise: clockwise)
|
||||
}
|
||||
}
|
||||
|
||||
extension Ellipse: DynamicNodeDecoding {
|
||||
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
|
||||
extension Ellipse: DynamicNodeEncoding {
|
||||
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
37
third-party/SwiftSVG/Sources/Extensions/Point+SwiftSVG.swift
vendored
Normal file
37
third-party/SwiftSVG/Sources/Extensions/Point+SwiftSVG.swift
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import Swift2D
|
||||
|
||||
extension Point {
|
||||
static var nan: Point {
|
||||
Point(x: Double.nan, y: Double.nan)
|
||||
}
|
||||
|
||||
var hasNaN: Bool {
|
||||
x.isNaN || y.isNaN
|
||||
}
|
||||
|
||||
/// Returns a copy of the instance with the **x** value replaced with the provided value.
|
||||
func with(x value: Double) -> Point {
|
||||
Point(x: value, y: y)
|
||||
}
|
||||
|
||||
/// Returns a copy of the instance with the **y** value replaced with the provided value.
|
||||
func with(y value: Double) -> Point {
|
||||
Point(x: x, y: value)
|
||||
}
|
||||
|
||||
/// Adjusts the **x** value by the provided amount.
|
||||
///
|
||||
/// This will explicitly check for `.isNaN`, and if encountered, will simply
|
||||
/// use the provided value.
|
||||
func adjusting(x value: Double) -> Point {
|
||||
(x.isNaN) ? with(x: value) : with(x: x + value)
|
||||
}
|
||||
|
||||
/// Adjusts the **y** value by the provided amount.
|
||||
///
|
||||
/// This will explicitly check for `.isNaN`, and if encountered, will simply
|
||||
/// use the provided value.
|
||||
func adjusting(y value: Double) -> Point {
|
||||
(y.isNaN) ? with(y: value) : with(y: y + value)
|
||||
}
|
||||
}
|
||||
44
third-party/SwiftSVG/Sources/Fill.swift
vendored
Normal file
44
third-party/SwiftSVG/Sources/Fill.swift
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import Swift2D
|
||||
|
||||
public struct Fill {
|
||||
|
||||
public var color: String?
|
||||
public var opacity: Double?
|
||||
public var rule: Rule = .nonZero
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Presentation attribute defining the algorithm to use to determine the inside part of a shape.
|
||||
///
|
||||
/// The default `Rule` is `.nonzero`.
|
||||
public enum Rule: String, Sendable, Codable, CaseIterable {
|
||||
/// The value evenodd determines the "insideness" of a point in the shape by drawing a ray from that point to
|
||||
/// infinity in any direction and counting the number of path segments from the given shape that the ray
|
||||
/// crosses. If this number is odd, the point is inside; if even, the point is outside.
|
||||
case evenOdd = "evenodd"
|
||||
/// The value nonzero determines the "insideness" of a point in the shape by drawing a ray from that point to
|
||||
/// infinity in any direction, and then examining the places where a segment of the shape crosses the ray.
|
||||
/// Starting with a count of zero, add one each time a path segment crosses the ray from left to right and
|
||||
/// subtract one each time a path segment crosses the ray from right to left. After counting the crossings, if
|
||||
/// the result is zero then the point is outside the path. Otherwise, it is inside.
|
||||
case nonZero = "nonzero"
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let rawValue = try container.decode(String.self)
|
||||
guard let rule = Rule(rawValue: rawValue) else {
|
||||
print("Attempts to decode Fill.Rule with rawValue: '\(rawValue)'")
|
||||
self = .nonZero
|
||||
return
|
||||
}
|
||||
|
||||
self = rule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Fill.Rule: CustomStringConvertible {
|
||||
public var description: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
152
third-party/SwiftSVG/Sources/Group.swift
vendored
Normal file
152
third-party/SwiftSVG/Sources/Group.swift
vendored
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import XMLCoder
|
||||
|
||||
/// A container used to group other SVG elements.
|
||||
///
|
||||
/// Grouping constructs, when used in conjunction with the ‘desc’ and ‘title’ elements, provide information
|
||||
/// about document structure and semantics.
|
||||
///
|
||||
/// ## Documentation
|
||||
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g)
|
||||
/// | [W3](https://www.w3.org/TR/SVG11/struct.html#Groups)
|
||||
public struct Group: Container, Element {
|
||||
|
||||
// Container
|
||||
public var circles: [Circle]?
|
||||
public var ellipses: [Ellipse]?
|
||||
public var groups: [Group]?
|
||||
public var lines: [Line]?
|
||||
public var paths: [Path]?
|
||||
public var polygons: [Polygon]?
|
||||
public var polylines: [Polyline]?
|
||||
public var rectangles: [Rectangle]?
|
||||
public var texts: [Text]?
|
||||
|
||||
// MARK: CoreAttributes
|
||||
|
||||
public var id: String?
|
||||
public var title: String?
|
||||
public var desc: String?
|
||||
|
||||
// MARK: PresentationAttributes
|
||||
|
||||
public var fillColor: String?
|
||||
public var fillOpacity: Double?
|
||||
public var fillRule: Fill.Rule?
|
||||
public var strokeColor: String?
|
||||
public var strokeWidth: Double?
|
||||
public var strokeOpacity: Double?
|
||||
public var strokeLineCap: Stroke.LineCap?
|
||||
public var strokeLineJoin: Stroke.LineJoin?
|
||||
public var strokeMiterLimit: Double?
|
||||
public var transform: String?
|
||||
|
||||
// MARK: StylingAttributes
|
||||
|
||||
public var style: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case circles = "circle"
|
||||
case ellipses = "ellipse"
|
||||
case groups = "g"
|
||||
case lines = "line"
|
||||
case paths = "path"
|
||||
case polylines = "polyline"
|
||||
case polygons = "polygon"
|
||||
case rectangles = "rect"
|
||||
case texts = "text"
|
||||
case id
|
||||
case title
|
||||
case desc
|
||||
case fillColor = "fill"
|
||||
case fillOpacity = "fill-opacity"
|
||||
case fillRule = "fill-rule"
|
||||
case strokeColor = "stroke"
|
||||
case strokeWidth = "stroke-width"
|
||||
case strokeOpacity = "stroke-opacity"
|
||||
case strokeLineCap = "stroke-linecap"
|
||||
case strokeLineJoin = "stroke-linejoin"
|
||||
case strokeMiterLimit = "stroke-miterlimit"
|
||||
case transform
|
||||
case style
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
/// A representation of all the sub-`Path`s in the `Group`.
|
||||
public func subpaths(applying transformations: [Transformation] = []) throws -> [Path] {
|
||||
var _transformations = transformations
|
||||
_transformations.append(contentsOf: self.transformations)
|
||||
|
||||
var output: [Path] = []
|
||||
|
||||
if let circles {
|
||||
try output.append(contentsOf: circles.compactMap { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let ellipses {
|
||||
try output.append(contentsOf: ellipses.compactMap { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let rectangles {
|
||||
try output.append(contentsOf: rectangles.compactMap { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let polygons {
|
||||
try output.append(contentsOf: polygons.compactMap { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let polylines {
|
||||
try output.append(contentsOf: polylines.compactMap { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let paths {
|
||||
try output.append(contentsOf: paths.map { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let groups {
|
||||
try groups.forEach {
|
||||
try output.append(contentsOf: $0.subpaths(applying: _transformations))
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
extension Group: CustomStringConvertible {
|
||||
public var description: String {
|
||||
var contents: String = ""
|
||||
|
||||
if let title {
|
||||
contents.append("\n<title>\(title)</title>")
|
||||
}
|
||||
|
||||
if let desc {
|
||||
contents.append("\n<desc>\(desc)</desc>")
|
||||
}
|
||||
|
||||
contents.append(containerDescription)
|
||||
|
||||
return "<g \(attributeDescription) >\(contents)\n</g>"
|
||||
}
|
||||
}
|
||||
|
||||
extension Group: DynamicNodeDecoding {
|
||||
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
|
||||
if let _ = ContainerKeys(stringValue: key.stringValue) {
|
||||
return .element
|
||||
}
|
||||
|
||||
return .attribute
|
||||
}
|
||||
}
|
||||
|
||||
extension Group: DynamicNodeEncoding {
|
||||
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
|
||||
if let _ = ContainerKeys(stringValue: key.stringValue) {
|
||||
return .element
|
||||
}
|
||||
|
||||
return .attribute
|
||||
}
|
||||
}
|
||||
88
third-party/SwiftSVG/Sources/Internal/EllipseProcessor.swift
vendored
Normal file
88
third-party/SwiftSVG/Sources/Internal/EllipseProcessor.swift
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import Foundation
|
||||
import Swift2D
|
||||
|
||||
struct EllipseProcessor {
|
||||
|
||||
let x: Double
|
||||
let y: Double
|
||||
let rx: Double
|
||||
let ry: Double
|
||||
|
||||
/// The _optimal_ offset for control points when representing a
|
||||
/// circle/ellipse as 4 bezier curves.
|
||||
///
|
||||
/// [Stack Overflow](https://stackoverflow.com/questions/1734745/how-to-create-circle-with-bézier-curves)
|
||||
static func controlPointOffset(_ radius: Double) -> Double {
|
||||
(Double(4.0 / 3.0) * tan(Double.pi / 8.0)) * radius
|
||||
}
|
||||
|
||||
init(ellipse: Ellipse) {
|
||||
x = ellipse.x
|
||||
y = ellipse.y
|
||||
rx = ellipse.rx
|
||||
ry = ellipse.ry
|
||||
}
|
||||
|
||||
init(circle: Circle) {
|
||||
x = circle.x
|
||||
y = circle.y
|
||||
rx = circle.r
|
||||
ry = circle.r
|
||||
}
|
||||
|
||||
func commands(clockwise: Bool) -> [Path.Command] {
|
||||
var commands: [Path.Command] = []
|
||||
|
||||
let xOffset = Self.controlPointOffset(rx)
|
||||
let yOffset = Self.controlPointOffset(ry)
|
||||
|
||||
let zero = Point(x: x + rx, y: y)
|
||||
let ninety = Point(x: x, y: y - ry)
|
||||
let oneEighty = Point(x: x - rx, y: y)
|
||||
let twoSeventy = Point(x: x, y: y + ry)
|
||||
|
||||
var cp1: Point = .zero
|
||||
var cp2: Point = .zero
|
||||
|
||||
// Starting at degree 0 (the right most point)
|
||||
commands.append(.moveTo(point: zero))
|
||||
|
||||
if clockwise {
|
||||
cp1 = Point(x: zero.x, y: zero.y + yOffset)
|
||||
cp2 = Point(x: twoSeventy.x + xOffset, y: twoSeventy.y)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: twoSeventy))
|
||||
|
||||
cp1 = Point(x: twoSeventy.x - xOffset, y: twoSeventy.y)
|
||||
cp2 = Point(x: oneEighty.x, y: oneEighty.y + yOffset)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: oneEighty))
|
||||
|
||||
cp1 = Point(x: oneEighty.x, y: oneEighty.y - yOffset)
|
||||
cp2 = Point(x: ninety.x - xOffset, y: ninety.y)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: ninety))
|
||||
|
||||
cp1 = Point(x: ninety.x + xOffset, y: ninety.y)
|
||||
cp2 = Point(x: zero.x, y: zero.y - yOffset)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: zero))
|
||||
} else {
|
||||
cp1 = Point(x: zero.x, y: zero.y - yOffset)
|
||||
cp2 = Point(x: ninety.x + xOffset, y: ninety.y)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: ninety))
|
||||
|
||||
cp1 = Point(x: ninety.x - xOffset, y: ninety.y)
|
||||
cp2 = Point(x: oneEighty.x, y: oneEighty.y - yOffset)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: oneEighty))
|
||||
|
||||
cp1 = Point(x: oneEighty.x, y: oneEighty.y + yOffset)
|
||||
cp2 = Point(x: twoSeventy.x - xOffset, y: twoSeventy.y)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: twoSeventy))
|
||||
|
||||
cp1 = Point(x: twoSeventy.x + xOffset, y: twoSeventy.y)
|
||||
cp2 = Point(x: zero.x, y: zero.y + yOffset)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: zero))
|
||||
}
|
||||
|
||||
commands.append(.closePath)
|
||||
|
||||
return commands
|
||||
}
|
||||
}
|
||||
12
third-party/SwiftSVG/Sources/Internal/PathProcessor.swift
vendored
Normal file
12
third-party/SwiftSVG/Sources/Internal/PathProcessor.swift
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import Foundation
|
||||
|
||||
struct PathProcessor {
|
||||
|
||||
let data: String
|
||||
|
||||
func commands() throws -> [Path.Command] {
|
||||
let parser = Path.ComponentParser()
|
||||
let components = try Path.Component.components(from: data)
|
||||
return try parser.parse(components)
|
||||
}
|
||||
}
|
||||
52
third-party/SwiftSVG/Sources/Internal/PolygonProcressor.swift
vendored
Normal file
52
third-party/SwiftSVG/Sources/Internal/PolygonProcressor.swift
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import Foundation
|
||||
import Swift2D
|
||||
|
||||
struct PolygonProcessor {
|
||||
|
||||
let points: String
|
||||
|
||||
func commands() throws -> [Path.Command] {
|
||||
let pairs = points.components(separatedBy: " ")
|
||||
let components = pairs.flatMap { $0.components(separatedBy: ",") }
|
||||
guard components.count > 0 else {
|
||||
return []
|
||||
}
|
||||
|
||||
guard components.count % 2 == 0 else {
|
||||
// An odd number of components means that parsing probably failed
|
||||
return []
|
||||
}
|
||||
|
||||
var commands: [Path.Command] = []
|
||||
|
||||
var firstValue: Bool = true
|
||||
for (idx, component) in components.enumerated() {
|
||||
guard let _value = Double(component) else {
|
||||
return commands
|
||||
}
|
||||
|
||||
let value = Double(_value)
|
||||
|
||||
if firstValue {
|
||||
if idx == 0 {
|
||||
commands.append(.moveTo(point: Point(x: value, y: .nan)))
|
||||
} else {
|
||||
commands.append(.lineTo(point: Point(x: value, y: .nan)))
|
||||
}
|
||||
firstValue = false
|
||||
} else {
|
||||
let count = commands.count
|
||||
guard let modified = try? commands.last?.adjustingArgument(at: 1, by: value) else {
|
||||
return commands
|
||||
}
|
||||
|
||||
commands[count - 1] = modified
|
||||
firstValue = true
|
||||
}
|
||||
}
|
||||
|
||||
commands.append(.closePath)
|
||||
|
||||
return commands
|
||||
}
|
||||
}
|
||||
54
third-party/SwiftSVG/Sources/Internal/PolylineProcessor.swift
vendored
Normal file
54
third-party/SwiftSVG/Sources/Internal/PolylineProcessor.swift
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import Foundation
|
||||
import Swift2D
|
||||
|
||||
struct PolylineProcessor {
|
||||
|
||||
let points: String
|
||||
|
||||
func commands() throws -> [Path.Command] {
|
||||
let pairs = points.components(separatedBy: " ")
|
||||
let components = pairs.flatMap { $0.components(separatedBy: ",") }
|
||||
let values = components.compactMap { Double($0) }.map { Double($0) }
|
||||
|
||||
guard values.count > 2 else {
|
||||
// More than just a starting point is required.
|
||||
return []
|
||||
}
|
||||
|
||||
guard values.count % 2 == 0 else {
|
||||
// An odd number of components means that parsing probably failed
|
||||
return []
|
||||
}
|
||||
|
||||
var commands: [Path.Command] = []
|
||||
|
||||
let move = values.prefix(upTo: 2)
|
||||
let segments = values.suffix(from: 2)
|
||||
|
||||
commands.append(.moveTo(point: Point(x: move[0], y: move[1])))
|
||||
|
||||
var _value: Double = .nan
|
||||
for value in segments {
|
||||
if _value.isNaN {
|
||||
_value = value
|
||||
} else {
|
||||
commands.append(.lineTo(point: Point(x: _value, y: value)))
|
||||
_value = .nan
|
||||
}
|
||||
}
|
||||
|
||||
let reversedSegments = segments.dropLast(2).reversed()
|
||||
for value in reversedSegments {
|
||||
if _value.isNaN {
|
||||
_value = value
|
||||
} else {
|
||||
commands.append(.lineTo(point: Point(x: _value, y: value)))
|
||||
_value = .nan
|
||||
}
|
||||
}
|
||||
|
||||
commands.append(.closePath)
|
||||
|
||||
return commands
|
||||
}
|
||||
}
|
||||
175
third-party/SwiftSVG/Sources/Internal/RectangleProcessor.swift
vendored
Normal file
175
third-party/SwiftSVG/Sources/Internal/RectangleProcessor.swift
vendored
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import Swift2D
|
||||
|
||||
struct RectangleProcessor {
|
||||
|
||||
let rectangle: Rectangle
|
||||
|
||||
func commands(clockwise: Bool) -> [Path.Command] {
|
||||
var rx = rectangle.rx
|
||||
var ry = rectangle.ry
|
||||
|
||||
if let _rx = rx, _rx > (rectangle.width / 2.0) {
|
||||
rx = rectangle.width / 2.0
|
||||
}
|
||||
|
||||
if let _ry = ry, _ry > (rectangle.height / 2.0) {
|
||||
ry = rectangle.height / 2.0
|
||||
}
|
||||
|
||||
var commands: [Path.Command] = []
|
||||
|
||||
switch (rx, ry) {
|
||||
case (.some(let radiusX), .some(let radiusY)) where radiusX != radiusY:
|
||||
// Use Cubic Bezier Curve to form rounded corners
|
||||
// TODO: Verify that the control points are right
|
||||
|
||||
var cp1: Point = .zero
|
||||
var cp2: Point = .zero
|
||||
var point: Point = Point(x: rectangle.x + radiusX, y: rectangle.y)
|
||||
|
||||
commands.append(.moveTo(point: point))
|
||||
|
||||
if clockwise {
|
||||
point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
|
||||
cp2 = cp1
|
||||
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radiusY)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
|
||||
|
||||
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radiusY)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
|
||||
cp2 = cp1
|
||||
point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y + rectangle.height)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
|
||||
|
||||
point = .init(x: rectangle.x + radiusX, y: rectangle.y + rectangle.height)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp1 = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
|
||||
cp2 = cp1
|
||||
point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radiusY)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
|
||||
|
||||
point = .init(x: rectangle.x, y: rectangle.y + radiusY)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp1 = .init(x: rectangle.x, y: rectangle.y)
|
||||
cp2 = cp1
|
||||
point = .init(x: rectangle.x + radiusX, y: rectangle.y)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
|
||||
} else {
|
||||
cp1 = .init(x: rectangle.x, y: rectangle.y)
|
||||
cp2 = cp1
|
||||
point = .init(x: rectangle.x, y: rectangle.y + radiusY)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
|
||||
|
||||
point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radiusY)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp1 = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
|
||||
cp2 = cp1
|
||||
point = .init(x: rectangle.x + radiusX, y: rectangle.y + rectangle.height)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
|
||||
|
||||
point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y + rectangle.height)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
|
||||
cp2 = cp1
|
||||
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radiusY)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
|
||||
|
||||
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radiusY)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp1 = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
|
||||
cp2 = cp1
|
||||
point = .init(x: rectangle.x + rectangle.width - radiusX, y: rectangle.y)
|
||||
commands.append(.cubicBezierCurve(cp1: cp1, cp2: cp2, point: point))
|
||||
}
|
||||
case (.some(let radius), .none), (.none, .some(let radius)), (.some(let radius), _):
|
||||
// use Quadratic Bezier Curve to form rounded corners
|
||||
|
||||
var cp: Point = .zero
|
||||
var point: Point = Point(x: rectangle.x + radius, y: rectangle.y)
|
||||
|
||||
commands.append(.moveTo(point: point))
|
||||
|
||||
if clockwise {
|
||||
point = .init(x: (rectangle.x + rectangle.width) - radius, y: rectangle.y)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
|
||||
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radius)
|
||||
commands.append(.quadraticBezierCurve(cp: cp, point: point))
|
||||
|
||||
point = .init(x: rectangle.x + rectangle.width, y: (rectangle.y + rectangle.height) - radius)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
|
||||
point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y + rectangle.height)
|
||||
commands.append(.quadraticBezierCurve(cp: cp, point: point))
|
||||
|
||||
point = .init(x: rectangle.x + radius, y: rectangle.y + rectangle.height)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
|
||||
point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radius)
|
||||
commands.append(.quadraticBezierCurve(cp: cp, point: point))
|
||||
|
||||
point = .init(x: rectangle.x, y: rectangle.y + radius)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp = .init(x: rectangle.x, y: rectangle.y)
|
||||
point = .init(x: rectangle.x + radius, y: rectangle.y)
|
||||
commands.append(.quadraticBezierCurve(cp: cp, point: point))
|
||||
} else {
|
||||
cp = .init(x: rectangle.x, y: rectangle.y)
|
||||
point = .init(x: rectangle.x, y: rectangle.y + radius)
|
||||
commands.append(.quadraticBezierCurve(cp: cp, point: point))
|
||||
|
||||
point = .init(x: rectangle.x, y: rectangle.y + rectangle.height - radius)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp = .init(x: rectangle.x, y: rectangle.y + rectangle.height)
|
||||
point = .init(x: rectangle.x + radius, y: rectangle.y + rectangle.height)
|
||||
commands.append(.quadraticBezierCurve(cp: cp, point: point))
|
||||
|
||||
point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y + rectangle.height)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)
|
||||
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height - radius)
|
||||
commands.append(.quadraticBezierCurve(cp: cp, point: point))
|
||||
|
||||
point = .init(x: rectangle.x + rectangle.width, y: rectangle.y + radius)
|
||||
commands.append(.lineTo(point: point))
|
||||
|
||||
cp = .init(x: rectangle.x + rectangle.width, y: rectangle.y)
|
||||
point = .init(x: rectangle.x + rectangle.width - radius, y: rectangle.y)
|
||||
commands.append(.quadraticBezierCurve(cp: cp, point: point))
|
||||
}
|
||||
case (.none, .none):
|
||||
// draw three line segments.
|
||||
commands.append(.moveTo(point: Point(x: rectangle.x, y: rectangle.y)))
|
||||
|
||||
if clockwise {
|
||||
commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y)))
|
||||
commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)))
|
||||
commands.append(.lineTo(point: Point(x: rectangle.x, y: rectangle.y + rectangle.height)))
|
||||
} else {
|
||||
commands.append(.lineTo(point: Point(x: rectangle.x, y: rectangle.y + rectangle.height)))
|
||||
commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height)))
|
||||
commands.append(.lineTo(point: Point(x: rectangle.x + rectangle.width, y: rectangle.y)))
|
||||
}
|
||||
}
|
||||
|
||||
commands.append(.closePath)
|
||||
|
||||
return commands
|
||||
}
|
||||
}
|
||||
98
third-party/SwiftSVG/Sources/Line.swift
vendored
Normal file
98
third-party/SwiftSVG/Sources/Line.swift
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import Swift2D
|
||||
import XMLCoder
|
||||
|
||||
/// SVG basic shape used to create a line connecting two points.
|
||||
///
|
||||
/// ## Documentation
|
||||
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
|
||||
/// | [W3](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)
|
||||
public struct Line: Element {
|
||||
|
||||
/// Defines the x-axis coordinate of the line starting point.
|
||||
public var x1: Double = 0.0
|
||||
/// Defines the x-axis coordinate of the line ending point.
|
||||
public var y1: Double = 0.0
|
||||
/// Defines the y-axis coordinate of the line starting point.
|
||||
public var x2: Double = 0.0
|
||||
/// Defines the y-axis coordinate of the line ending point.
|
||||
public var y2: Double = 0.0
|
||||
|
||||
// MARK: CoreAttributes
|
||||
|
||||
public var id: String?
|
||||
|
||||
// MARK: PresentationAttributes
|
||||
|
||||
public var fillColor: String?
|
||||
public var fillOpacity: Double?
|
||||
public var fillRule: Fill.Rule?
|
||||
public var strokeColor: String?
|
||||
public var strokeWidth: Double?
|
||||
public var strokeOpacity: Double?
|
||||
public var strokeLineCap: Stroke.LineCap?
|
||||
public var strokeLineJoin: Stroke.LineJoin?
|
||||
public var strokeMiterLimit: Double?
|
||||
public var transform: String?
|
||||
|
||||
// MARK: StylingAttributes
|
||||
|
||||
public var style: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case x1
|
||||
case y1
|
||||
case x2
|
||||
case y2
|
||||
case id
|
||||
case fillColor = "fill"
|
||||
case fillOpacity = "fill-opacity"
|
||||
case fillRule = "fill-rule"
|
||||
case strokeColor = "stroke"
|
||||
case strokeWidth = "stroke-width"
|
||||
case strokeOpacity = "stroke-opacity"
|
||||
case strokeLineCap = "stroke-linecap"
|
||||
case strokeLineJoin = "stroke-linejoin"
|
||||
case strokeMiterLimit = "stroke-miterlimit"
|
||||
case transform
|
||||
case style
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(x1: Double, y1: Double, x2: Double, y2: Double) {
|
||||
self.x1 = x1
|
||||
self.y1 = y1
|
||||
self.x2 = x2
|
||||
self.y2 = y2
|
||||
}
|
||||
}
|
||||
|
||||
extension Line: CommandRepresentable {
|
||||
public func commands() throws -> [Path.Command] {
|
||||
[
|
||||
.moveTo(point: Point(x: x1, y: y1)),
|
||||
.lineTo(point: Point(x: x2, y: y2)),
|
||||
.lineTo(point: Point(x: x1, y: y1)),
|
||||
.closePath,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
extension Line: CustomStringConvertible {
|
||||
public var description: String {
|
||||
let desc = "<line x1=\"\(x1)\" y1=\"\(y1)\" x2=\"\(x2)\" y2=\"\(y2)\""
|
||||
return desc + " \(attributeDescription) />"
|
||||
}
|
||||
}
|
||||
|
||||
extension Line: DynamicNodeDecoding {
|
||||
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
|
||||
extension Line: DynamicNodeEncoding {
|
||||
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
276
third-party/SwiftSVG/Sources/Path.Command.swift
vendored
Normal file
276
third-party/SwiftSVG/Sources/Path.Command.swift
vendored
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
import Foundation
|
||||
import Swift2D
|
||||
|
||||
public extension Path {
|
||||
/// Path commands are instructions that define a path to be drawn.
|
||||
///
|
||||
/// Each command is composed of a command letter and numbers that represent the command parameters.
|
||||
enum Command: Equatable, Sendable, CustomStringConvertible {
|
||||
/// Moves the current drawing point
|
||||
case moveTo(point: Point)
|
||||
/// Draw a straight line from the current point to the point provided
|
||||
case lineTo(point: Point)
|
||||
/// Draw a smooth curve using three points (+ origin)
|
||||
case cubicBezierCurve(cp1: Point, cp2: Point, point: Point)
|
||||
/// Draw a smooth curve using two points (+ origin)
|
||||
case quadraticBezierCurve(cp: Point, point: Point)
|
||||
/// Draw a curve defined as a portion of an ellipse
|
||||
case ellipticalArcCurve(rx: Double, ry: Double, angle: Double, largeArc: Bool, clockwise: Bool, point: Point)
|
||||
/// ClosePath instructions draw a straight line from the current position to the first point in the path.
|
||||
case closePath
|
||||
|
||||
public enum Prefix: Character, CaseIterable {
|
||||
case move = "M"
|
||||
case relativeMove = "m"
|
||||
case line = "L"
|
||||
case relativeLine = "l"
|
||||
case horizontalLine = "H"
|
||||
case relativeHorizontalLine = "h"
|
||||
case verticalLine = "V"
|
||||
case relativeVerticalLine = "v"
|
||||
case cubicBezierCurve = "C"
|
||||
case relativeCubicBezierCurve = "c"
|
||||
case smoothCubicBezierCurve = "S"
|
||||
case relativeSmoothCubicBezierCurve = "s"
|
||||
case quadraticBezierCurve = "Q"
|
||||
case relativeQuadraticBezierCurve = "q"
|
||||
case smoothQuadraticBezierCurve = "T"
|
||||
case relativeSmoothQuadraticBezierCurve = "t"
|
||||
case ellipticalArcCurve = "A"
|
||||
case relativeEllipticalArcCurve = "a"
|
||||
case close = "Z"
|
||||
case relativeClose = "z"
|
||||
|
||||
public static var characterSet: CharacterSet {
|
||||
CharacterSet(charactersIn: allCases.map { String($0.rawValue) }.joined())
|
||||
}
|
||||
}
|
||||
|
||||
public enum Coordinates {
|
||||
case absolute
|
||||
case relative
|
||||
}
|
||||
|
||||
public enum Error: Swift.Error {
|
||||
case message(String)
|
||||
case invalidAdjustment(Path.Command)
|
||||
case invalidArgumentPosition(Int, Path.Command)
|
||||
case invalidRelativeCommand
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .moveTo(let point):
|
||||
return "\(Prefix.move.rawValue)\(point.x),\(point.y)"
|
||||
case .lineTo(let point):
|
||||
return "\(Prefix.line.rawValue)\(point.x),\(point.y)"
|
||||
case .cubicBezierCurve(let cp1, let cp2, let point):
|
||||
return "\(Prefix.cubicBezierCurve.rawValue)\(cp1.x),\(cp1.y) \(cp2.x),\(cp2.y) \(point.x),\(point.y)"
|
||||
case .quadraticBezierCurve(let cp, let point):
|
||||
return "\(Prefix.quadraticBezierCurve.rawValue)\(cp.x),\(cp.y) \(point.x),\(point.y)"
|
||||
case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
|
||||
let la = largeArc ? 1 : 0
|
||||
let cw = clockwise ? 1 : 0
|
||||
return "\(Prefix.ellipticalArcCurve.rawValue)\(rx) \(ry) \(angle) \(la) \(cw) \(point.x) \(point.y)"
|
||||
case .closePath:
|
||||
return "\(Prefix.close.rawValue)"
|
||||
}
|
||||
}
|
||||
|
||||
/// The primary point that dictates the commands action.
|
||||
public var point: Point {
|
||||
switch self {
|
||||
case .moveTo(let point): point
|
||||
case .lineTo(let point): point
|
||||
case .cubicBezierCurve(_, _, let point): point
|
||||
case .quadraticBezierCurve(_, let point): point
|
||||
case .ellipticalArcCurve(_, _, _, _, _, let point): point
|
||||
case .closePath: .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Path.Command {
|
||||
/// Applies the provided `Transformation` to the instances values.
|
||||
func applying(transformation: Transformation) -> Path.Command {
|
||||
switch transformation {
|
||||
case .translate(let x, let y):
|
||||
switch self {
|
||||
case .moveTo(let point):
|
||||
let _point = point.adjusting(x: x).adjusting(y: y)
|
||||
return .moveTo(point: _point)
|
||||
case .lineTo(let point):
|
||||
let _point = point.adjusting(x: x).adjusting(y: y)
|
||||
return .lineTo(point: _point)
|
||||
case .cubicBezierCurve(let cp1, let cp2, let point):
|
||||
let _cp1 = cp1.adjusting(x: x).adjusting(y: y)
|
||||
let _cp2 = cp2.adjusting(x: x).adjusting(y: y)
|
||||
let _point = point.adjusting(x: x).adjusting(y: y)
|
||||
return .cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point)
|
||||
case .quadraticBezierCurve(let cp, let point):
|
||||
let _cp = cp.adjusting(x: x).adjusting(y: y)
|
||||
let _point = point.adjusting(x: x).adjusting(y: y)
|
||||
return .quadraticBezierCurve(cp: _cp, point: _point)
|
||||
case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
|
||||
let _point = point.adjusting(x: x).adjusting(y: y)
|
||||
return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: _point)
|
||||
case .closePath:
|
||||
return self
|
||||
}
|
||||
case .matrix:
|
||||
// TODO: What should occur here?
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies multiple transformations in the order they are specified.
|
||||
func applying(transformations: [Transformation]) -> Path.Command {
|
||||
var command = self
|
||||
|
||||
for transformation in transformations {
|
||||
command = command.applying(transformation: transformation)
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
}
|
||||
|
||||
extension Path.Command {
|
||||
/// Determines if all values are provided (i.e. !.isNaN)
|
||||
var isComplete: Bool {
|
||||
switch self {
|
||||
case .moveTo(let point), .lineTo(let point):
|
||||
!point.hasNaN
|
||||
case .cubicBezierCurve(let cp1, let cp2, let point):
|
||||
!cp1.hasNaN && !cp2.hasNaN && !point.hasNaN
|
||||
case .quadraticBezierCurve(let cp, let point):
|
||||
!cp.hasNaN && !point.hasNaN
|
||||
case .ellipticalArcCurve(let rx, let ry, let angle, _, _, let point):
|
||||
!rx.isNaN && !ry.isNaN && !angle.isNaN && !point.hasNaN
|
||||
case .closePath:
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// The last control point used in drawing the path.
|
||||
///
|
||||
/// Only valid for curves.
|
||||
var lastControlPoint: Point? {
|
||||
switch self {
|
||||
case .cubicBezierCurve(_, let cp2, _):
|
||||
cp2
|
||||
case .quadraticBezierCurve(let cp, _):
|
||||
cp
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
/// A mirror representation of `lastControlPoint`.
|
||||
var lastControlPointMirror: Point? {
|
||||
guard let cp = lastControlPoint else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Point(x: point.x + (point.x - cp.x), y: point.y + (point.y - cp.y))
|
||||
}
|
||||
|
||||
/// The total number of argument values the command requires.
|
||||
var arguments: Int {
|
||||
switch self {
|
||||
case .moveTo: 2
|
||||
case .lineTo: 2
|
||||
case .cubicBezierCurve: 6
|
||||
case .quadraticBezierCurve: 4
|
||||
case .ellipticalArcCurve: 7
|
||||
case .closePath: 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjusts a Command argument by a specified amount.
|
||||
///
|
||||
/// A `Point` consumes two positions. So, in the example `.quadraticBezierCurve(cp: .zero, point: .zero)`:
|
||||
/// * position 0 = Control Point X
|
||||
/// * position 1 = Control Point Y
|
||||
/// * position 2 = Point X
|
||||
/// * position 3 = Point Y
|
||||
///
|
||||
/// - parameter position: The index of the argument parameter to adjust.
|
||||
/// - parameter value: The value to add to the existing value. If the current value equal `.isNaN`, than the
|
||||
/// supplied value is used as-is.
|
||||
/// - throws: `Path.Command.Error`
|
||||
func adjustingArgument(at position: Int, by value: Double) throws -> Path.Command {
|
||||
switch self {
|
||||
case .moveTo(let point):
|
||||
switch position {
|
||||
case 0:
|
||||
return .moveTo(point: point.adjusting(x: value))
|
||||
case 1:
|
||||
return .moveTo(point: point.adjusting(y: value))
|
||||
default:
|
||||
throw Path.Command.Error.invalidArgumentPosition(position, self)
|
||||
}
|
||||
case .lineTo(let point):
|
||||
switch position {
|
||||
case 0:
|
||||
return .lineTo(point: point.adjusting(x: value))
|
||||
case 1:
|
||||
return .lineTo(point: point.adjusting(y: value))
|
||||
default:
|
||||
throw Path.Command.Error.invalidArgumentPosition(position, self)
|
||||
}
|
||||
case .cubicBezierCurve(let cp1, let cp2, let point):
|
||||
switch position {
|
||||
case 0:
|
||||
return .cubicBezierCurve(cp1: cp1.adjusting(x: value), cp2: cp2, point: point)
|
||||
case 1:
|
||||
return .cubicBezierCurve(cp1: cp1.adjusting(y: value), cp2: cp2, point: point)
|
||||
case 2:
|
||||
return .cubicBezierCurve(cp1: cp1, cp2: cp2.adjusting(x: value), point: point)
|
||||
case 3:
|
||||
return .cubicBezierCurve(cp1: cp1, cp2: cp2.adjusting(y: value), point: point)
|
||||
case 4:
|
||||
return .cubicBezierCurve(cp1: cp1, cp2: cp2, point: point.adjusting(x: value))
|
||||
case 5:
|
||||
return .cubicBezierCurve(cp1: cp1, cp2: cp2, point: point.adjusting(y: value))
|
||||
default:
|
||||
throw Path.Command.Error.invalidArgumentPosition(position, self)
|
||||
}
|
||||
case .quadraticBezierCurve(let cp, let point):
|
||||
switch position {
|
||||
case 0:
|
||||
return .quadraticBezierCurve(cp: cp.adjusting(x: value), point: point)
|
||||
case 1:
|
||||
return .quadraticBezierCurve(cp: cp.adjusting(y: value), point: point)
|
||||
case 2:
|
||||
return .quadraticBezierCurve(cp: cp, point: point.adjusting(x: value))
|
||||
case 3:
|
||||
return .quadraticBezierCurve(cp: cp, point: point.adjusting(y: value))
|
||||
default:
|
||||
throw Path.Command.Error.invalidArgumentPosition(position, self)
|
||||
}
|
||||
case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
|
||||
switch position {
|
||||
case 0:
|
||||
return .ellipticalArcCurve(rx: value, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point)
|
||||
case 1:
|
||||
return .ellipticalArcCurve(rx: rx, ry: value, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point)
|
||||
case 2:
|
||||
return .ellipticalArcCurve(rx: rx, ry: ry, angle: value, largeArc: largeArc, clockwise: clockwise, point: point)
|
||||
case 3:
|
||||
return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: !value.isZero, clockwise: clockwise, point: point)
|
||||
case 4:
|
||||
return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: !value.isZero, point: point)
|
||||
case 5:
|
||||
return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point.adjusting(x: value))
|
||||
case 6:
|
||||
return .ellipticalArcCurve(rx: rx, ry: ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: point.adjusting(y: value))
|
||||
default:
|
||||
throw Path.Command.Error.invalidArgumentPosition(position, self)
|
||||
}
|
||||
case .closePath:
|
||||
throw Path.Command.Error.invalidAdjustment(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
98
third-party/SwiftSVG/Sources/Path.Component.swift
vendored
Normal file
98
third-party/SwiftSVG/Sources/Path.Component.swift
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import Foundation
|
||||
|
||||
public extension Path {
|
||||
/// A unit of a SVG path data string.
|
||||
enum Component {
|
||||
case prefix(Command.Prefix)
|
||||
case value(Double)
|
||||
|
||||
/// Interprets a `Path` `data` attribute into individual `Component`s for command processing.
|
||||
public static func components(from data: String) throws -> [Component] {
|
||||
var blocks: [String] = []
|
||||
var block: String = ""
|
||||
|
||||
for scalar in data.unicodeScalars {
|
||||
if scalar == "e" {
|
||||
// Account for exponential value notation.
|
||||
block.append(String(scalar))
|
||||
continue
|
||||
}
|
||||
|
||||
if CharacterSet.letters.contains(scalar) {
|
||||
if !block.isEmpty {
|
||||
blocks.append(block)
|
||||
block = ""
|
||||
}
|
||||
|
||||
blocks.append(String(scalar))
|
||||
continue
|
||||
}
|
||||
|
||||
if CharacterSet.whitespaces.contains(scalar) {
|
||||
if !block.isEmpty {
|
||||
blocks.append(block)
|
||||
block = ""
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if CharacterSet(charactersIn: ",").contains(scalar) {
|
||||
if !block.isEmpty {
|
||||
blocks.append(block)
|
||||
block = ""
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if CharacterSet(charactersIn: "-").contains(scalar) {
|
||||
if !block.isEmpty, block.last != "e" {
|
||||
// Again, account for exponential values.
|
||||
blocks.append(block)
|
||||
block = ""
|
||||
}
|
||||
|
||||
block.append(String(scalar))
|
||||
continue
|
||||
}
|
||||
|
||||
if CharacterSet(charactersIn: ".").contains(scalar) {
|
||||
if block.contains(".") {
|
||||
// Already decimal value, this is a new value
|
||||
blocks.append(block)
|
||||
block = ""
|
||||
}
|
||||
|
||||
block.append(String(scalar))
|
||||
continue
|
||||
}
|
||||
|
||||
if CharacterSet.decimalDigits.contains(scalar) {
|
||||
block.append(String(scalar))
|
||||
continue
|
||||
}
|
||||
|
||||
print("Unhandled Character: \(scalar)")
|
||||
}
|
||||
|
||||
if !block.isEmpty {
|
||||
blocks.append(block)
|
||||
block = ""
|
||||
}
|
||||
|
||||
return blocks
|
||||
.filter { !$0.isEmpty }
|
||||
.compactMap {
|
||||
if let prefix = Path.Command.Prefix(rawValue: $0.first!) {
|
||||
.prefix(prefix)
|
||||
} else if let value = Double($0) {
|
||||
.value(value)
|
||||
} else {
|
||||
// throw in the future?
|
||||
nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
275
third-party/SwiftSVG/Sources/Path.ComponentParser.swift
vendored
Normal file
275
third-party/SwiftSVG/Sources/Path.ComponentParser.swift
vendored
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
import Swift2D
|
||||
|
||||
public extension Path {
|
||||
/// Utility used to construct a collection of `Path.Command` from a collection of `Path.Component`.
|
||||
class ComponentParser {
|
||||
/// The command currently being built
|
||||
private var command: Path.Command?
|
||||
/// Coordinate system being used
|
||||
private var coordinates: Path.Command.Coordinates = .absolute
|
||||
/// The argument position of the _command_ to be processed.
|
||||
private var position: Int = 0
|
||||
/// Indicates that only a single value will be processed on the next component pass.
|
||||
private var singleValue: Bool = false
|
||||
/// The originating coordinates of the path.
|
||||
private var pathOrigin: Point = .nan
|
||||
/// The last point as processed by the parser.
|
||||
private var currentPoint: Point = .zero
|
||||
|
||||
public init() {}
|
||||
|
||||
public func parse(_ components: [Path.Component]) throws -> [Path.Command] {
|
||||
var commands: [Path.Command] = []
|
||||
|
||||
try components.forEach { component in
|
||||
if let command = try parse(component, lastCommand: commands.last) {
|
||||
commands.append(command)
|
||||
}
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
|
||||
private func parse(_ component: Path.Component, lastCommand: Path.Command?) throws -> Path.Command? {
|
||||
switch component {
|
||||
case .prefix(let prefix):
|
||||
setup(prefix: prefix, lastCommand: lastCommand)
|
||||
case .value(let value):
|
||||
try process(value: value, lastCommand: lastCommand)
|
||||
}
|
||||
}
|
||||
|
||||
private func setup(prefix: Path.Command.Prefix, lastCommand: Path.Command?) -> Path.Command? {
|
||||
position = 0
|
||||
singleValue = false
|
||||
|
||||
switch prefix {
|
||||
case .move:
|
||||
command = .moveTo(point: .nan)
|
||||
coordinates = .absolute
|
||||
case .relativeMove:
|
||||
command = .moveTo(point: currentPoint)
|
||||
coordinates = .relative
|
||||
case .line:
|
||||
command = .lineTo(point: .nan)
|
||||
coordinates = .absolute
|
||||
case .relativeLine:
|
||||
command = .lineTo(point: currentPoint)
|
||||
coordinates = .relative
|
||||
case .horizontalLine:
|
||||
command = .lineTo(point: currentPoint.with(x: .nan))
|
||||
coordinates = .absolute
|
||||
case .relativeHorizontalLine:
|
||||
command = .lineTo(point: currentPoint)
|
||||
coordinates = .relative
|
||||
singleValue = true
|
||||
case .verticalLine:
|
||||
command = .lineTo(point: currentPoint.with(y: .nan))
|
||||
coordinates = .absolute
|
||||
position = 1
|
||||
case .relativeVerticalLine:
|
||||
command = .lineTo(point: currentPoint)
|
||||
coordinates = .relative
|
||||
position = 1
|
||||
singleValue = true
|
||||
case .cubicBezierCurve:
|
||||
command = .cubicBezierCurve(cp1: .nan, cp2: .nan, point: .nan)
|
||||
coordinates = .absolute
|
||||
case .relativeCubicBezierCurve:
|
||||
command = .cubicBezierCurve(cp1: currentPoint, cp2: currentPoint, point: currentPoint)
|
||||
coordinates = .relative
|
||||
case .smoothCubicBezierCurve:
|
||||
if case .cubicBezierCurve(_, let cp, _) = lastCommand {
|
||||
command = .cubicBezierCurve(cp1: cp.reflecting(around: currentPoint), cp2: .nan, point: .nan)
|
||||
} else {
|
||||
command = .cubicBezierCurve(cp1: currentPoint, cp2: .nan, point: .nan)
|
||||
}
|
||||
coordinates = .absolute
|
||||
position = 2
|
||||
case .relativeSmoothCubicBezierCurve:
|
||||
if case .cubicBezierCurve(_, let cp, _) = lastCommand {
|
||||
command = .cubicBezierCurve(cp1: cp.reflecting(around: cp.reflecting(around: currentPoint)), cp2: currentPoint, point: currentPoint)
|
||||
} else {
|
||||
command = .cubicBezierCurve(cp1: currentPoint, cp2: currentPoint, point: currentPoint)
|
||||
}
|
||||
coordinates = .relative
|
||||
position = 2
|
||||
case .quadraticBezierCurve:
|
||||
command = .quadraticBezierCurve(cp: .nan, point: .nan)
|
||||
coordinates = .absolute
|
||||
case .relativeQuadraticBezierCurve:
|
||||
command = .quadraticBezierCurve(cp: currentPoint, point: currentPoint)
|
||||
coordinates = .relative
|
||||
case .smoothQuadraticBezierCurve:
|
||||
if case .quadraticBezierCurve(let cp, _) = lastCommand {
|
||||
command = .quadraticBezierCurve(cp: cp.reflecting(around: currentPoint), point: .nan)
|
||||
} else {
|
||||
command = .quadraticBezierCurve(cp: currentPoint, point: .nan)
|
||||
}
|
||||
coordinates = .absolute
|
||||
position = 2
|
||||
case .relativeSmoothQuadraticBezierCurve:
|
||||
if case .quadraticBezierCurve(let cp, _) = lastCommand {
|
||||
command = .quadraticBezierCurve(cp: cp.reflecting(around: currentPoint), point: currentPoint)
|
||||
} else {
|
||||
command = .quadraticBezierCurve(cp: currentPoint, point: currentPoint)
|
||||
}
|
||||
coordinates = .relative
|
||||
position = 2
|
||||
case .ellipticalArcCurve:
|
||||
command = .ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: .nan)
|
||||
coordinates = .absolute
|
||||
case .relativeEllipticalArcCurve:
|
||||
command = .ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: currentPoint)
|
||||
coordinates = .relative
|
||||
case .close, .relativeClose:
|
||||
currentPoint = pathOrigin
|
||||
reset()
|
||||
return .closePath
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func process(value: Double, lastCommand: Path.Command?) throws -> Path.Command? {
|
||||
if let command {
|
||||
try continueCommand(command, with: value)
|
||||
} else {
|
||||
try nextCommand(with: value, lastCommand: lastCommand)
|
||||
}
|
||||
|
||||
if let command, command.isComplete {
|
||||
switch coordinates {
|
||||
case .relative:
|
||||
guard position == -1 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
fallthrough
|
||||
case .absolute:
|
||||
currentPoint = command.point
|
||||
if case .moveTo = command {
|
||||
pathOrigin = command.point
|
||||
}
|
||||
reset()
|
||||
return command
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func continueCommand(_ command: Path.Command, with value: Double) throws {
|
||||
switch command {
|
||||
case .moveTo, .cubicBezierCurve, .quadraticBezierCurve, .ellipticalArcCurve:
|
||||
self.command = try command.adjustingArgument(at: position, by: value)
|
||||
switch coordinates {
|
||||
case .absolute:
|
||||
position += 1
|
||||
case .relative:
|
||||
switch position {
|
||||
case 0 ... (command.arguments - 2):
|
||||
position += 1
|
||||
case command.arguments - 1:
|
||||
position = -1
|
||||
default:
|
||||
break // throw?
|
||||
}
|
||||
}
|
||||
case .lineTo:
|
||||
self.command = try command.adjustingArgument(at: position, by: value)
|
||||
switch coordinates {
|
||||
case .absolute:
|
||||
position += 1
|
||||
case .relative:
|
||||
switch position {
|
||||
case 0:
|
||||
if singleValue {
|
||||
singleValue = false
|
||||
position = -1
|
||||
} else {
|
||||
position += 1
|
||||
}
|
||||
case 1:
|
||||
if singleValue {
|
||||
singleValue = false
|
||||
}
|
||||
position = -1
|
||||
default:
|
||||
break // throw?
|
||||
}
|
||||
}
|
||||
case .closePath:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func nextCommand(with value: Double, lastCommand: Path.Command?) throws {
|
||||
guard let command = lastCommand else {
|
||||
throw Path.Command.Error.invalidRelativeCommand
|
||||
}
|
||||
|
||||
switch command {
|
||||
case .moveTo:
|
||||
switch coordinates {
|
||||
case .absolute:
|
||||
self.command = .lineTo(point: Point(x: value, y: .nan))
|
||||
position = 1
|
||||
case .relative:
|
||||
let c = Path.Command.lineTo(point: command.point)
|
||||
self.command = try c.adjustingArgument(at: 0, by: value)
|
||||
position = 1
|
||||
}
|
||||
case .lineTo:
|
||||
switch coordinates {
|
||||
case .absolute:
|
||||
self.command = .lineTo(point: Point(x: value, y: .nan))
|
||||
position = 1
|
||||
case .relative:
|
||||
let c = Path.Command.lineTo(point: command.point)
|
||||
self.command = try c.adjustingArgument(at: 0, by: value)
|
||||
position = 1
|
||||
}
|
||||
case .cubicBezierCurve:
|
||||
switch coordinates {
|
||||
case .absolute:
|
||||
self.command = .cubicBezierCurve(cp1: Point(x: value, y: .nan), cp2: .nan, point: .nan)
|
||||
position = 1
|
||||
case .relative:
|
||||
let c = Path.Command.cubicBezierCurve(cp1: command.point, cp2: command.point, point: command.point)
|
||||
self.command = try c.adjustingArgument(at: 0, by: value)
|
||||
position = 1
|
||||
}
|
||||
case .quadraticBezierCurve:
|
||||
switch coordinates {
|
||||
case .absolute:
|
||||
self.command = .quadraticBezierCurve(cp: Point(x: value, y: .nan), point: .nan)
|
||||
position = 1
|
||||
case .relative:
|
||||
let c = Path.Command.quadraticBezierCurve(cp: command.point, point: command.point)
|
||||
self.command = try c.adjustingArgument(at: 0, by: value)
|
||||
position = 1
|
||||
}
|
||||
case .ellipticalArcCurve:
|
||||
switch coordinates {
|
||||
case .absolute:
|
||||
self.command = .ellipticalArcCurve(rx: value, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: .nan)
|
||||
position = 1
|
||||
case .relative:
|
||||
let c = Path.Command.ellipticalArcCurve(rx: .nan, ry: .nan, angle: .nan, largeArc: false, clockwise: false, point: command.point)
|
||||
self.command = try c.adjustingArgument(at: 0, by: value)
|
||||
position = 1
|
||||
}
|
||||
case .closePath:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
command = nil
|
||||
coordinates = .absolute
|
||||
position = 0
|
||||
singleValue = false
|
||||
}
|
||||
}
|
||||
}
|
||||
101
third-party/SwiftSVG/Sources/Path.swift
vendored
Normal file
101
third-party/SwiftSVG/Sources/Path.swift
vendored
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import XMLCoder
|
||||
|
||||
/// Generic element to define a shape.
|
||||
///
|
||||
/// A path is defined by including a ‘path’ element in a SVG document which contains a **d="(path data)"**
|
||||
/// attribute, where the **‘d’** attribute contains the moveto, line, curve (both Cubic and Quadratic Bézier),
|
||||
/// arc and closepath instructions.
|
||||
///
|
||||
/// ## Documentation
|
||||
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)
|
||||
/// | [W3](https://www.w3.org/TR/SVG11/paths.html)
|
||||
public struct Path: Element {
|
||||
|
||||
/// The definition of the outline of a shape.
|
||||
public var data: String = ""
|
||||
|
||||
// MARK: CoreAttributes
|
||||
|
||||
public var id: String?
|
||||
|
||||
// MARK: PresentationAttributes
|
||||
|
||||
public var fillColor: String?
|
||||
public var fillOpacity: Double?
|
||||
public var fillRule: Fill.Rule?
|
||||
public var strokeColor: String?
|
||||
public var strokeWidth: Double?
|
||||
public var strokeOpacity: Double?
|
||||
public var strokeLineCap: Stroke.LineCap?
|
||||
public var strokeLineJoin: Stroke.LineJoin?
|
||||
public var strokeMiterLimit: Double?
|
||||
public var transform: String?
|
||||
|
||||
// MARK: StylingAttributes
|
||||
|
||||
public var style: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case data = "d"
|
||||
case id
|
||||
case fillColor = "fill"
|
||||
case fillOpacity = "fill-opacity"
|
||||
case fillRule = "fill-rule"
|
||||
case strokeColor = "stroke"
|
||||
case strokeWidth = "stroke-width"
|
||||
case strokeOpacity = "stroke-opacity"
|
||||
case strokeLineCap = "stroke-linecap"
|
||||
case strokeLineJoin = "stroke-linejoin"
|
||||
case strokeMiterLimit = "stroke-miterlimit"
|
||||
case transform
|
||||
case style
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(data: String) {
|
||||
self.init()
|
||||
self.data = data
|
||||
}
|
||||
|
||||
public init(commands: [Path.Command]) {
|
||||
self.init()
|
||||
data = commands.map(\.description).joined()
|
||||
}
|
||||
}
|
||||
|
||||
extension Path: CommandRepresentable {
|
||||
public func commands() throws -> [Command] {
|
||||
try PathProcessor(data: data).commands()
|
||||
}
|
||||
}
|
||||
|
||||
extension Path: CustomStringConvertible {
|
||||
public var description: String {
|
||||
"<path d=\"\(data)\" \(attributeDescription) />"
|
||||
}
|
||||
}
|
||||
|
||||
extension Path: DynamicNodeDecoding {
|
||||
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
|
||||
extension Path: DynamicNodeEncoding {
|
||||
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
|
||||
extension Path: Equatable {
|
||||
public static func == (lhs: Path, rhs: Path) -> Bool {
|
||||
do {
|
||||
let lhsCommands = try lhs.commands()
|
||||
let rhsCommands = try rhs.commands()
|
||||
return lhsCommands == rhsCommands
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
82
third-party/SwiftSVG/Sources/Polygon.swift
vendored
Normal file
82
third-party/SwiftSVG/Sources/Polygon.swift
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import XMLCoder
|
||||
|
||||
/// Defines a closed shape consisting of a set of connected straight line segments.
|
||||
///
|
||||
/// The last point is connected to the first point. For open shapes, see the `Polyline` element. If an odd number of
|
||||
/// coordinates is provided, then the element is in error.
|
||||
///
|
||||
/// ## Documentation
|
||||
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon)
|
||||
/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#PolygonElement)
|
||||
public struct Polygon: Element {
|
||||
|
||||
/// The points that make up the polygon.
|
||||
public var points: String = ""
|
||||
|
||||
// MARK: CoreAttributes
|
||||
|
||||
public var id: String?
|
||||
|
||||
// MARK: PresentationAttributes
|
||||
|
||||
public var fillColor: String?
|
||||
public var fillOpacity: Double?
|
||||
public var fillRule: Fill.Rule?
|
||||
public var strokeColor: String?
|
||||
public var strokeWidth: Double?
|
||||
public var strokeOpacity: Double?
|
||||
public var strokeLineCap: Stroke.LineCap?
|
||||
public var strokeLineJoin: Stroke.LineJoin?
|
||||
public var strokeMiterLimit: Double?
|
||||
public var transform: String?
|
||||
|
||||
// MARK: StylingAttributes
|
||||
|
||||
public var style: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case points
|
||||
case id
|
||||
case fillColor = "fill"
|
||||
case fillOpacity = "fill-opacity"
|
||||
case fillRule = "fill-rule"
|
||||
case strokeColor = "stroke"
|
||||
case strokeWidth = "stroke-width"
|
||||
case strokeOpacity = "stroke-opacity"
|
||||
case strokeLineCap = "stroke-linecap"
|
||||
case strokeLineJoin = "stroke-linejoin"
|
||||
case strokeMiterLimit = "stroke-miterlimit"
|
||||
case transform
|
||||
case style
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(points: String) {
|
||||
self.points = points
|
||||
}
|
||||
}
|
||||
|
||||
extension Polygon: CommandRepresentable {
|
||||
public func commands() throws -> [Path.Command] {
|
||||
try PolygonProcessor(points: points).commands()
|
||||
}
|
||||
}
|
||||
|
||||
extension Polygon: CustomStringConvertible {
|
||||
public var description: String {
|
||||
"<polygon points=\"\(points)\" \(attributeDescription) />"
|
||||
}
|
||||
}
|
||||
|
||||
extension Polygon: DynamicNodeDecoding {
|
||||
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
|
||||
extension Polygon: DynamicNodeEncoding {
|
||||
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
81
third-party/SwiftSVG/Sources/Polyline.swift
vendored
Normal file
81
third-party/SwiftSVG/Sources/Polyline.swift
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import XMLCoder
|
||||
|
||||
/// SVG basic shape that creates straight lines connecting several points.
|
||||
///
|
||||
/// Typically a polyline is used to create open shapes as the last point doesn't have to be connected to the first
|
||||
/// point. For closed shapes see the `Polygon` element.
|
||||
///
|
||||
/// ## Documentation
|
||||
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline)
|
||||
/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#PolylineElement)
|
||||
public struct Polyline: Element {
|
||||
|
||||
public var points: String = ""
|
||||
|
||||
// MARK: CoreAttributes
|
||||
|
||||
public var id: String?
|
||||
|
||||
// MARK: PresentationAttributes
|
||||
|
||||
public var fillColor: String?
|
||||
public var fillOpacity: Double?
|
||||
public var fillRule: Fill.Rule?
|
||||
public var strokeColor: String?
|
||||
public var strokeWidth: Double?
|
||||
public var strokeOpacity: Double?
|
||||
public var strokeLineCap: Stroke.LineCap?
|
||||
public var strokeLineJoin: Stroke.LineJoin?
|
||||
public var strokeMiterLimit: Double?
|
||||
public var transform: String?
|
||||
|
||||
// MARK: StylingAttributes
|
||||
|
||||
public var style: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case points
|
||||
case id
|
||||
case fillColor = "fill"
|
||||
case fillOpacity = "fill-opacity"
|
||||
case fillRule = "fill-rule"
|
||||
case strokeColor = "stroke"
|
||||
case strokeWidth = "stroke-width"
|
||||
case strokeOpacity = "stroke-opacity"
|
||||
case strokeLineCap = "stroke-linecap"
|
||||
case strokeLineJoin = "stroke-linejoin"
|
||||
case strokeMiterLimit = "stroke-miterlimit"
|
||||
case transform
|
||||
case style
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(points: String) {
|
||||
self.points = points
|
||||
}
|
||||
}
|
||||
|
||||
extension Polyline: CommandRepresentable {
|
||||
public func commands() throws -> [Path.Command] {
|
||||
try PolylineProcessor(points: points).commands()
|
||||
}
|
||||
}
|
||||
|
||||
extension Polyline: CustomStringConvertible {
|
||||
public var description: String {
|
||||
"<polyline points=\"\(points)\" \(attributeDescription) />"
|
||||
}
|
||||
}
|
||||
|
||||
extension Polyline: DynamicNodeDecoding {
|
||||
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
|
||||
extension Polyline: DynamicNodeEncoding {
|
||||
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
120
third-party/SwiftSVG/Sources/PresentationAttributes.swift
vendored
Normal file
120
third-party/SwiftSVG/Sources/PresentationAttributes.swift
vendored
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import Foundation
|
||||
import Swift2D
|
||||
|
||||
public protocol PresentationAttributes {
|
||||
var fillColor: String? { get set }
|
||||
var fillOpacity: Double? { get set }
|
||||
var fillRule: Fill.Rule? { get set }
|
||||
var strokeColor: String? { get set }
|
||||
var strokeWidth: Double? { get set }
|
||||
var strokeOpacity: Double? { get set }
|
||||
var strokeLineCap: Stroke.LineCap? { get set }
|
||||
var strokeLineJoin: Stroke.LineJoin? { get set }
|
||||
var strokeMiterLimit: Double? { get set }
|
||||
var transform: String? { get set }
|
||||
}
|
||||
|
||||
enum PresentationAttributesKeys: String, CodingKey {
|
||||
case fillColor = "fill"
|
||||
case fillOpacity = "fill-opacity"
|
||||
case fillRule = "fill-rule"
|
||||
case strokeColor = "stroke"
|
||||
case strokeWidth = "stroke-width"
|
||||
case strokeOpacity = "stroke-opacity"
|
||||
case strokeLineCap = "stroke-linecap"
|
||||
case strokeLineJoin = "stroke-linejoin"
|
||||
case strokeMiterLimit = "stroke-miterlimit"
|
||||
case transform
|
||||
}
|
||||
|
||||
public extension PresentationAttributes {
|
||||
var presentationDescription: String {
|
||||
var attributes: [String] = []
|
||||
|
||||
if let fillColor {
|
||||
attributes.append("\(PresentationAttributesKeys.fillColor.rawValue)=\"\(fillColor)\"")
|
||||
}
|
||||
if let fillOpacity {
|
||||
attributes.append("\(PresentationAttributesKeys.fillOpacity.rawValue)=\"\(fillOpacity)\"")
|
||||
}
|
||||
if let fillRule {
|
||||
attributes.append("\(PresentationAttributesKeys.fillRule.rawValue)=\"\(fillRule.description)\"")
|
||||
}
|
||||
if let strokeColor {
|
||||
attributes.append("\(PresentationAttributesKeys.strokeColor.rawValue)=\"\(strokeColor)\"")
|
||||
}
|
||||
if let strokeWidth {
|
||||
attributes.append("\(PresentationAttributesKeys.strokeWidth.rawValue)=\"\(strokeWidth)\"")
|
||||
}
|
||||
if let strokeOpacity {
|
||||
attributes.append("\(PresentationAttributesKeys.strokeOpacity.rawValue)=\"\(strokeOpacity)\"")
|
||||
}
|
||||
if let strokeLineCap {
|
||||
attributes.append("\(PresentationAttributesKeys.strokeLineCap.rawValue)=\"\(strokeLineCap.description)\"")
|
||||
}
|
||||
if let strokeLineJoin {
|
||||
attributes.append("\(PresentationAttributesKeys.strokeLineJoin.rawValue)=\"\(strokeLineJoin.description)\"")
|
||||
}
|
||||
if let strokeMiterLimit {
|
||||
attributes.append("\(PresentationAttributesKeys.strokeMiterLimit.rawValue)=\"\(strokeMiterLimit)\"")
|
||||
}
|
||||
if let transform {
|
||||
attributes.append("\(PresentationAttributesKeys.transform.rawValue)=\"\(transform)\"")
|
||||
}
|
||||
|
||||
return attributes.joined(separator: " ")
|
||||
}
|
||||
|
||||
var transformations: [Transformation] {
|
||||
let value = transform?.replacingOccurrences(of: " ", with: "") ?? ""
|
||||
guard !value.isEmpty else {
|
||||
return []
|
||||
}
|
||||
|
||||
let values = value.split(separator: ")").map { $0.appending(")") }
|
||||
return values.compactMap { Transformation($0) }
|
||||
}
|
||||
|
||||
var fill: Fill? {
|
||||
get {
|
||||
if fillColor == nil, fillOpacity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var fill = Fill()
|
||||
fill.color = fillColor ?? "black"
|
||||
fill.opacity = fillOpacity ?? 1.0
|
||||
return fill
|
||||
}
|
||||
set {
|
||||
fillColor = newValue?.color
|
||||
fillOpacity = newValue?.opacity
|
||||
fillRule = newValue?.rule
|
||||
}
|
||||
}
|
||||
|
||||
var stroke: Stroke? {
|
||||
get {
|
||||
if strokeColor == nil, strokeOpacity == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var stroke = Stroke()
|
||||
stroke.color = strokeColor ?? "black"
|
||||
stroke.opacity = strokeOpacity ?? 1.0
|
||||
stroke.width = strokeWidth ?? 1.0
|
||||
stroke.lineCap = strokeLineCap ?? .butt
|
||||
stroke.lineJoin = strokeLineJoin ?? .miter
|
||||
stroke.miterLimit = strokeMiterLimit
|
||||
return stroke
|
||||
}
|
||||
set {
|
||||
strokeColor = newValue?.color
|
||||
strokeOpacity = newValue?.opacity
|
||||
strokeWidth = newValue?.width
|
||||
strokeLineCap = newValue?.lineCap
|
||||
strokeLineJoin = newValue?.lineJoin
|
||||
strokeMiterLimit = newValue?.miterLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
115
third-party/SwiftSVG/Sources/Rectangle.swift
vendored
Normal file
115
third-party/SwiftSVG/Sources/Rectangle.swift
vendored
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import Swift2D
|
||||
import XMLCoder
|
||||
|
||||
/// Basic SVG shape that draws rectangles, defined by their position, width, and height.
|
||||
///
|
||||
/// The values used for the x- and y-axis rounded corner radii are determined implicitly
|
||||
/// if the ‘rx’ or ‘ry’ attributes (or both) are not specified, or are specified but with invalid values.
|
||||
///
|
||||
/// ## Documentation
|
||||
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect)
|
||||
/// | [W3](https://www.w3.org/TR/SVG11/shapes.html#RectElement)
|
||||
public struct Rectangle: Element {
|
||||
|
||||
/// The x-axis coordinate of the side of the rectangle which
|
||||
/// has the smaller x-axis coordinate value.
|
||||
public var x: Double = 0.0
|
||||
/// The y-axis coordinate of the side of the rectangle which
|
||||
/// has the smaller y-axis coordinate value
|
||||
public var y: Double = 0.0
|
||||
/// The width of the rectangle.
|
||||
public var width: Double = 0.0
|
||||
/// The height of the rectangle.
|
||||
public var height: Double = 0.0
|
||||
/// For rounded rectangles, the x-axis radius of the ellipse used
|
||||
/// to round off the corners of the rectangle.
|
||||
public var rx: Double?
|
||||
/// For rounded rectangles, the y-axis radius of the ellipse used
|
||||
/// to round off the corners of the rectangle.
|
||||
public var ry: Double?
|
||||
|
||||
// MARK: CoreAttributes
|
||||
|
||||
public var id: String?
|
||||
|
||||
// MARK: PresentationAttributes
|
||||
|
||||
public var fillColor: String?
|
||||
public var fillOpacity: Double?
|
||||
public var fillRule: Fill.Rule?
|
||||
public var strokeColor: String?
|
||||
public var strokeWidth: Double?
|
||||
public var strokeOpacity: Double?
|
||||
public var strokeLineCap: Stroke.LineCap?
|
||||
public var strokeLineJoin: Stroke.LineJoin?
|
||||
public var strokeMiterLimit: Double?
|
||||
public var transform: String?
|
||||
|
||||
// MARK: StylingAttributes
|
||||
|
||||
public var style: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case x
|
||||
case y
|
||||
case width
|
||||
case height
|
||||
case rx
|
||||
case ry
|
||||
case id
|
||||
case fillColor = "fill"
|
||||
case fillOpacity = "fill-opacity"
|
||||
case fillRule = "fill-rule"
|
||||
case strokeColor = "stroke"
|
||||
case strokeWidth = "stroke-width"
|
||||
case strokeOpacity = "stroke-opacity"
|
||||
case strokeLineCap = "stroke-linecap"
|
||||
case strokeLineJoin = "stroke-linejoin"
|
||||
case strokeMiterLimit = "stroke-miterlimit"
|
||||
case transform
|
||||
case style
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(x: Double, y: Double, width: Double, height: Double, rx: Double? = nil, ry: Double? = nil) {
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.rx = rx
|
||||
self.ry = ry
|
||||
}
|
||||
}
|
||||
|
||||
extension Rectangle: CustomStringConvertible {
|
||||
public var description: String {
|
||||
var desc = "<rect x=\"\(x)\" y=\"\(y)\" width=\"\(width)\" height=\"\(height)\""
|
||||
if let rx {
|
||||
desc.append(" rx=\"\(rx)\"")
|
||||
}
|
||||
if let ry {
|
||||
desc.append(" ry=\"\(ry)\"")
|
||||
}
|
||||
|
||||
return desc + " \(attributeDescription) />"
|
||||
}
|
||||
}
|
||||
|
||||
extension Rectangle: DirectionalCommandRepresentable {
|
||||
public func commands(clockwise: Bool) throws -> [Path.Command] {
|
||||
RectangleProcessor(rectangle: self).commands(clockwise: clockwise)
|
||||
}
|
||||
}
|
||||
|
||||
extension Rectangle: DynamicNodeDecoding {
|
||||
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
|
||||
extension Rectangle: DynamicNodeEncoding {
|
||||
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
62
third-party/SwiftSVG/Sources/SVG+Swift2D.swift
vendored
Normal file
62
third-party/SwiftSVG/Sources/SVG+Swift2D.swift
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import Foundation
|
||||
import Swift2D
|
||||
|
||||
public extension SVG {
|
||||
/// Original size of the document image.
|
||||
///
|
||||
/// Primarily uses the `viewBox` attribute, and will fallback to the 'pixelSize'
|
||||
var originalSize: Size {
|
||||
(viewBoxSize ?? pixelSize) ?? .zero
|
||||
}
|
||||
|
||||
/// Size of the design in a square 'viewBox'.
|
||||
///
|
||||
/// All paths created by this framework are outputted in a 'square'.
|
||||
var outputSize: Size {
|
||||
let size = (pixelSize ?? viewBoxSize) ?? .zero
|
||||
let maxDimension = max(size.width, size.height)
|
||||
return Size(width: maxDimension, height: maxDimension)
|
||||
}
|
||||
|
||||
/// Size derived from the `viewBox` document attribute
|
||||
var viewBoxSize: Size? {
|
||||
guard let viewBox else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let components = viewBox.components(separatedBy: .whitespaces)
|
||||
guard components.count == 4 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let width = Double(components[2]) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let height = Double(components[3]) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Size(width: width, height: height)
|
||||
}
|
||||
|
||||
/// Size derived from the 'width' & 'height' document attributes
|
||||
var pixelSize: Size? {
|
||||
guard let width, !width.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let height, !height.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let widthRawValue = width.replacingOccurrences(of: "px", with: "", options: .caseInsensitive, range: nil)
|
||||
let heightRawValue = height.replacingOccurrences(of: "px", with: "", options: .caseInsensitive, range: nil)
|
||||
|
||||
guard let w = Double(widthRawValue), let h = Double(heightRawValue) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Size(width: w, height: h)
|
||||
}
|
||||
}
|
||||
160
third-party/SwiftSVG/Sources/SVG.swift
vendored
Normal file
160
third-party/SwiftSVG/Sources/SVG.swift
vendored
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import Foundation
|
||||
import XMLCoder
|
||||
|
||||
/// SVG is a language for describing two-dimensional graphics in XML.
|
||||
///
|
||||
/// The svg element is a container that defines a new coordinate system and viewport. It is used as the outermost
|
||||
/// element of SVG documents, but it can also be used to embed a SVG fragment inside an SVG or HTML document.
|
||||
///
|
||||
/// ## Documentation
|
||||
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/svg)
|
||||
/// | [W3](https://www.w3.org/TR/SVG11/)
|
||||
public struct SVG: Container {
|
||||
|
||||
public var viewBox: String?
|
||||
public var width: String?
|
||||
public var height: String?
|
||||
public var title: String?
|
||||
public var desc: String?
|
||||
|
||||
// Container
|
||||
public var circles: [Circle]?
|
||||
public var ellipses: [Ellipse]?
|
||||
public var groups: [Group]?
|
||||
public var lines: [Line]?
|
||||
public var paths: [Path]?
|
||||
public var polygons: [Polygon]?
|
||||
public var polylines: [Polyline]?
|
||||
public var rectangles: [Rectangle]?
|
||||
public var texts: [Text]?
|
||||
|
||||
/// A non-optional, non-spaced representation of the `title`.
|
||||
public var name: String {
|
||||
let name = title ?? "SVG Document"
|
||||
let newTitle = name.components(separatedBy: .punctuationCharacters).joined(separator: "_")
|
||||
return newTitle.replacingOccurrences(of: " ", with: "_")
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case width
|
||||
case height
|
||||
case viewBox
|
||||
case title
|
||||
case desc
|
||||
case circles = "circle"
|
||||
case ellipses = "ellipse"
|
||||
case groups = "g"
|
||||
case lines = "line"
|
||||
case paths = "path"
|
||||
case polylines = "polyline"
|
||||
case polygons = "polygon"
|
||||
case rectangles = "rect"
|
||||
case texts = "text"
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(width: Int, height: Int) {
|
||||
self.width = "\(width)px"
|
||||
self.height = "\(height)px"
|
||||
viewBox = "0 0 \(width) \(height)"
|
||||
}
|
||||
|
||||
public static func make(from url: URL) throws -> SVG {
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
throw CocoaError(.fileNoSuchFile)
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: url)
|
||||
return try make(with: data)
|
||||
}
|
||||
|
||||
public static func make(with data: Data, decoder: XMLDecoder = XMLDecoder()) throws -> SVG {
|
||||
try decoder.decode(SVG.self, from: data)
|
||||
}
|
||||
|
||||
/// A collection of all `Path`s in the document.
|
||||
public func subpaths() throws -> [Path] {
|
||||
var output: [Path] = []
|
||||
let _transformations: [Transformation] = []
|
||||
|
||||
if let circles {
|
||||
try output.append(contentsOf: circles.compactMap { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let ellipses {
|
||||
try output.append(contentsOf: ellipses.compactMap { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let rectangles {
|
||||
try output.append(contentsOf: rectangles.compactMap { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let polygons {
|
||||
try output.append(contentsOf: polygons.compactMap { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let polylines {
|
||||
try output.append(contentsOf: polylines.compactMap { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let paths {
|
||||
try output.append(contentsOf: paths.map { try $0.path(applying: _transformations) })
|
||||
}
|
||||
|
||||
if let groups {
|
||||
try groups.forEach {
|
||||
try output.append(contentsOf: $0.subpaths(applying: _transformations))
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/// A singular path that represents all of the `Command`s within the document.
|
||||
public func coalescedPath() throws -> Path {
|
||||
let paths = try subpaths()
|
||||
let commands = try paths.flatMap { try $0.commands() }
|
||||
return Path(commands: commands)
|
||||
}
|
||||
}
|
||||
|
||||
extension SVG: CustomStringConvertible {
|
||||
public var description: String {
|
||||
var contents: String = ""
|
||||
|
||||
if let title {
|
||||
contents.append("\n<title>\(title)</title>")
|
||||
}
|
||||
|
||||
if let desc {
|
||||
contents.append("\n<desc>\(desc)</desc>")
|
||||
}
|
||||
|
||||
contents.append(containerDescription)
|
||||
|
||||
return "<svg viewBox=\"\(viewBox ?? "")\" width=\"\(width ?? "")\" height=\"\(height ?? "")\">\(contents)\n</svg>"
|
||||
}
|
||||
}
|
||||
|
||||
extension SVG: DynamicNodeDecoding {
|
||||
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
|
||||
switch key {
|
||||
case CodingKeys.width, CodingKeys.height, CodingKeys.viewBox:
|
||||
.attribute
|
||||
default:
|
||||
.element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SVG: DynamicNodeEncoding {
|
||||
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
|
||||
switch key {
|
||||
case CodingKeys.width, CodingKeys.height, CodingKeys.viewBox:
|
||||
.attribute
|
||||
default:
|
||||
.element
|
||||
}
|
||||
}
|
||||
}
|
||||
64
third-party/SwiftSVG/Sources/Stroke.swift
vendored
Normal file
64
third-party/SwiftSVG/Sources/Stroke.swift
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import Swift2D
|
||||
|
||||
public struct Stroke {
|
||||
|
||||
public var color: String?
|
||||
public var width: Double?
|
||||
public var opacity: Double?
|
||||
public var lineCap: LineCap = .butt
|
||||
public var lineJoin: LineJoin = .miter
|
||||
public var miterLimit: Double?
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Presentation attribute defining the shape to be used at the end of open subpaths when they are stroked.
|
||||
///
|
||||
/// The default `LineCap` is `.butt`
|
||||
public enum LineCap: String, Sendable, Codable, CaseIterable {
|
||||
/// The stroke for each subpath does not extend beyond its two endpoints.
|
||||
case butt
|
||||
/// The end of each subpath the stroke will be extended by a half circle with a diameter equal to the stroke
|
||||
/// width.
|
||||
case round
|
||||
/// The end of each subpath the stroke will be extended by a rectangle with a width equal to half the width of
|
||||
/// the stroke and a height equal to the width of the stroke.
|
||||
case square
|
||||
}
|
||||
|
||||
/// Presentation attribute defining the shape to be used at the corners of paths when they are stroked.
|
||||
///
|
||||
/// The default `LineJoin` is `.miter`
|
||||
public enum LineJoin: String, Sendable, Codable, CaseIterable {
|
||||
/// An arcs corner is to be used to join path segments.
|
||||
///
|
||||
/// The arcs shape is formed by extending the outer edges of the stroke at the join point with arcs that have
|
||||
/// the same curvature as the outer edges at the join point.
|
||||
case arcs
|
||||
/// The bevel value indicates that a bevelled corner is to be used to join path segments.
|
||||
case bevel
|
||||
/// Indicates that a sharp corner is to be used to join path segments.
|
||||
///
|
||||
/// The corner is formed by extending the outer edges of the stroke at the tangents of the path segments until
|
||||
/// they intersect.
|
||||
case miter
|
||||
/// A sharp corner is to be used to join path segments.
|
||||
///
|
||||
/// The corner is formed by extending the outer edges of the stroke at the tangents of the path segments until
|
||||
/// they intersect.
|
||||
case miterClip = "miter-clip"
|
||||
/// The round value indicates that a round corner is to be used to join path segments.
|
||||
case round
|
||||
}
|
||||
}
|
||||
|
||||
extension Stroke.LineCap: CustomStringConvertible {
|
||||
public var description: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension Stroke.LineJoin: CustomStringConvertible {
|
||||
public var description: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
17
third-party/SwiftSVG/Sources/StylingAttributes.swift
vendored
Normal file
17
third-party/SwiftSVG/Sources/StylingAttributes.swift
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
public protocol StylingAttributes {
|
||||
var style: String? { get set }
|
||||
}
|
||||
|
||||
enum StylingAttributesKeys: String, CodingKey {
|
||||
case style
|
||||
}
|
||||
|
||||
public extension StylingAttributes {
|
||||
var stylingDescription: String {
|
||||
if let style {
|
||||
"\(StylingAttributesKeys.style.rawValue)=\"\(style)\""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
110
third-party/SwiftSVG/Sources/Text.swift
vendored
Normal file
110
third-party/SwiftSVG/Sources/Text.swift
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import Foundation
|
||||
import XMLCoder
|
||||
|
||||
/// Graphics element consisting of text
|
||||
///
|
||||
/// It's possible to apply a gradient, pattern, clipping path, mask, or filter to `Text`, like any other SVG graphics element.
|
||||
///
|
||||
/// ## Documentation
|
||||
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text)
|
||||
/// | [W3](https://www.w3.org/TR/SVG11/text.html#TextElement)
|
||||
public struct Text: Element {
|
||||
|
||||
public var value: String = ""
|
||||
public var x: Double?
|
||||
public var y: Double?
|
||||
public var dx: Double?
|
||||
public var dy: Double?
|
||||
|
||||
// MARK: CoreAttributes
|
||||
|
||||
public var id: String?
|
||||
|
||||
// MARK: PresentationAttributes
|
||||
|
||||
public var fillColor: String?
|
||||
public var fillOpacity: Double?
|
||||
public var fillRule: Fill.Rule?
|
||||
public var strokeColor: String?
|
||||
public var strokeWidth: Double?
|
||||
public var strokeOpacity: Double?
|
||||
public var strokeLineCap: Stroke.LineCap?
|
||||
public var strokeLineJoin: Stroke.LineJoin?
|
||||
public var strokeMiterLimit: Double?
|
||||
public var transform: String?
|
||||
|
||||
// MARK: StylingAttributes
|
||||
|
||||
public var style: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case value = ""
|
||||
case x
|
||||
case y
|
||||
case dx
|
||||
case dy
|
||||
case id
|
||||
case fillColor = "fill"
|
||||
case fillOpacity = "fill-opacity"
|
||||
case fillRule = "fill-rule"
|
||||
case strokeColor = "stroke"
|
||||
case strokeWidth = "stroke-width"
|
||||
case strokeOpacity = "stroke-opacity"
|
||||
case strokeLineCap = "stroke-linecap"
|
||||
case strokeLineJoin = "stroke-linejoin"
|
||||
case strokeMiterLimit = "stroke-miterlimit"
|
||||
case transform
|
||||
case style
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public init(value: String) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
extension Text: CustomStringConvertible {
|
||||
public var description: String {
|
||||
var components: [String] = []
|
||||
|
||||
if let x, !x.isNaN, !x.isZero {
|
||||
components.append(String(format: "x=\"%.5f\"", x))
|
||||
}
|
||||
if let y, !y.isNaN, !y.isZero {
|
||||
components.append(String(format: "y=\"%.5f\"", y))
|
||||
}
|
||||
if let dx, !dx.isNaN, !dx.isZero {
|
||||
components.append(String(format: "dx=\"%.5f\"", dx))
|
||||
}
|
||||
if let dy, !dy.isNaN, !dy.isZero {
|
||||
components.append(String(format: "dy=\"%.5f\"", dy))
|
||||
}
|
||||
|
||||
components.append(attributeDescription)
|
||||
|
||||
return "<text " + components.joined(separator: " ") + " >\(value)</text>"
|
||||
}
|
||||
}
|
||||
|
||||
extension Text: DynamicNodeDecoding {
|
||||
public static func nodeDecoding(for key: any CodingKey) -> XMLDecoder.NodeDecoding {
|
||||
switch key {
|
||||
case CodingKeys.value:
|
||||
.element
|
||||
default:
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Text: DynamicNodeEncoding {
|
||||
public static func nodeEncoding(for key: any CodingKey) -> XMLEncoder.NodeEncoding {
|
||||
switch key {
|
||||
case CodingKeys.value:
|
||||
.element
|
||||
default:
|
||||
.attribute
|
||||
}
|
||||
}
|
||||
}
|
||||
113
third-party/SwiftSVG/Sources/Transformation.swift
vendored
Normal file
113
third-party/SwiftSVG/Sources/Transformation.swift
vendored
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import Foundation
|
||||
import Swift2D
|
||||
|
||||
/// A modification that should be applied to an element and its children.
|
||||
///
|
||||
/// If a list of transforms is provided, then the net effect is as if each transform had been specified separately in
|
||||
/// the order provided.
|
||||
///
|
||||
/// For example,
|
||||
/// ```
|
||||
/// <g transform="translate(-10,-20) scale(2) rotate(45) translate(5,10)">
|
||||
/// <!-- graphics elements go here -->
|
||||
/// </g>
|
||||
/// ```
|
||||
/// is functionally equivalent to:
|
||||
/// ```
|
||||
/// <g transform="translate(-10,-20)">
|
||||
/// <g transform="scale(2)">
|
||||
/// <g transform="rotate(45)">
|
||||
/// <g transform="translate(5,10)">
|
||||
/// <!-- graphics elements go here -->
|
||||
/// </g>
|
||||
/// </g>
|
||||
/// </g>
|
||||
/// </g>
|
||||
/// ```
|
||||
///
|
||||
/// The ‘transform’ attribute is applied to an element before processing any other coordinate or length values supplied
|
||||
/// for that element.
|
||||
///
|
||||
/// ## Documentation
|
||||
/// [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform)
|
||||
/// | [W3](https://www.w3.org/TR/SVG11/coords.html#TransformAttribute)
|
||||
public enum Transformation {
|
||||
/// Moves an object by x & y. (Y is assumed to be '0' if not provided)
|
||||
case translate(x: Double, y: Double)
|
||||
/// Specifies a transformation in the form of a transformation matrix of six values.
|
||||
case matrix(a: Double, b: Double, c: Double, d: Double, e: Double, f: Double)
|
||||
|
||||
public enum Prefix: String, CaseIterable {
|
||||
case translate
|
||||
case matrix
|
||||
}
|
||||
|
||||
/// Initializes a new `Transformation` with a raw SVG transformation string.
|
||||
public init?(_ string: String) {
|
||||
guard let prefix = Prefix.allCases.first(where: { string.lowercased().hasPrefix($0.rawValue) }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch prefix {
|
||||
case .translate:
|
||||
guard let start = string.firstIndex(of: "(") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let stop = string.lastIndex(of: ")") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var substring = String(string[start ... stop])
|
||||
substring = substring.replacingOccurrences(of: "(", with: "")
|
||||
substring = substring.replacingOccurrences(of: ")", with: "")
|
||||
|
||||
var components = substring.split(separator: " ", omittingEmptySubsequences: true).map { String($0) }
|
||||
components = components.flatMap { $0.components(separatedBy: ",") }
|
||||
|
||||
let values = components.compactMap { Double($0) }.map { Double($0) }
|
||||
guard values.count > 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if values.count > 1 {
|
||||
self = .translate(x: values[0], y: values[1])
|
||||
} else {
|
||||
self = .translate(x: values[0], y: 0.0)
|
||||
}
|
||||
case .matrix:
|
||||
guard let start = string.firstIndex(of: "(") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let stop = string.lastIndex(of: ")") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var substring = String(string[start ... stop])
|
||||
substring = substring.replacingOccurrences(of: "(", with: "")
|
||||
substring = substring.replacingOccurrences(of: ")", with: "")
|
||||
|
||||
var components = substring.split(separator: " ", omittingEmptySubsequences: true).map { String($0) }
|
||||
components = components.flatMap { $0.components(separatedBy: ",") }
|
||||
|
||||
let values = components.compactMap { Double($0) }.map { Double($0) }
|
||||
guard values.count > 5 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self = .matrix(a: values[0], b: values[1], c: values[2], d: values[3], e: values[4], f: values[5])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Transformation: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .translate(let x, let y):
|
||||
"translate(\(x), \(y))"
|
||||
case .matrix(let a, let b, let c, let d, let e, let f):
|
||||
"matrix(\(a), \(b), \(c), \(d), \(e), \(f))"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
third-party/VectorPlus/BUILD
vendored
Normal file
19
third-party/VectorPlus/BUILD
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "VectorPlus",
|
||||
module_name = "VectorPlus",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//third-party/SwiftColor",
|
||||
"//third-party/SwiftSVG",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
72
third-party/VectorPlus/Sources/CoreGraphics/CGContext+Render.swift
vendored
Normal file
72
third-party/VectorPlus/Sources/CoreGraphics/CGContext+Render.swift
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import Swift2D
|
||||
import SwiftColor
|
||||
import SwiftSVG
|
||||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
|
||||
public extension CGContext {
|
||||
func render(path: Path, from: Rect, to: Rect) throws {
|
||||
saveGState()
|
||||
|
||||
let cgPath = CGMutablePath()
|
||||
|
||||
let commands = (try? path.commands()) ?? []
|
||||
for (idx, command) in commands.enumerated() {
|
||||
let previous: Point? = if idx > 0 {
|
||||
commands[idx - 1].previousPoint
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
cgPath.addCommand(command, from: from, to: to, previousPoint: previous)
|
||||
}
|
||||
|
||||
if let fill = path.fill {
|
||||
let _color = Pigment(fill.color ?? "black")
|
||||
if _color.alpha != 0.0 {
|
||||
let cgColor = CGColor.make(_color)
|
||||
let color = cgColor.copy(alpha: CGFloat(fill.opacity ?? 1.0)) ?? cgColor
|
||||
let rule = fill.rule.cgFillRule
|
||||
|
||||
setFillColor(color)
|
||||
addPath(cgPath)
|
||||
fillPath(using: rule)
|
||||
}
|
||||
}
|
||||
|
||||
if let stroke = path.stroke {
|
||||
let _color = Pigment(stroke.color ?? "black")
|
||||
if _color.alpha != 0.0 {
|
||||
let cgColor = CGColor.make(_color)
|
||||
let color = cgColor.copy(alpha: CGFloat(stroke.opacity ?? 1.0)) ?? cgColor
|
||||
let width = stroke.width ?? 1.0
|
||||
let lineWidth = width * (to.size.width / from.size.width)
|
||||
|
||||
setLineWidth(CGFloat(lineWidth))
|
||||
setStrokeColor(color)
|
||||
setLineCap(stroke.lineCap.cgLineCap)
|
||||
setLineJoin(stroke.lineJoin.cgLineJoin)
|
||||
if let miterLimit = stroke.miterLimit, stroke.lineJoin == .miter {
|
||||
setMiterLimit(CGFloat(miterLimit))
|
||||
}
|
||||
addPath(cgPath)
|
||||
strokePath()
|
||||
}
|
||||
}
|
||||
|
||||
if path.fill == nil, path.stroke == nil {
|
||||
let color = CGColor(srgbRed: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
|
||||
setFillColor(color)
|
||||
addPath(cgPath)
|
||||
fillPath(using: path.fill?.rule.cgFillRule ?? .winding)
|
||||
}
|
||||
|
||||
restoreGState()
|
||||
}
|
||||
|
||||
func render(path: Path, in rect: Rect) throws {
|
||||
try render(path: path, from: rect, to: rect)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
45
third-party/VectorPlus/Sources/CoreGraphics/CGMutablePath+Instruction.swift
vendored
Normal file
45
third-party/VectorPlus/Sources/CoreGraphics/CGMutablePath+Instruction.swift
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import Swift2D
|
||||
import SwiftSVG
|
||||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
|
||||
public extension CGMutablePath {
|
||||
/// Adds a `Path.Command` to a _CoreGraphics path_, using the provided rectangles to correctly scale the parameters.
|
||||
///
|
||||
/// - parameter command: The `Path.Command` to append
|
||||
/// - parameter from: The `Rect` which originally had the instruction. This is typically the `Document.originalSize`.
|
||||
/// - parameter to: The `Rect` defining the new size.
|
||||
/// - parameter previousPoint: The last `Point`, used for Elliptical Arc calculations
|
||||
func addCommand(_ command: Path.Command, from: Rect, to: Rect, previousPoint: Point? = nil) {
|
||||
let translated = command.translate(from: from, to: to)
|
||||
switch translated {
|
||||
case .moveTo(let point):
|
||||
move(to: CGPoint(point))
|
||||
case .lineTo(let point):
|
||||
addLine(to: CGPoint(point))
|
||||
case .cubicBezierCurve(let cp1, let cp2, let point):
|
||||
addCurve(to: CGPoint(point), control1: CGPoint(cp1), control2: CGPoint(cp2))
|
||||
case .quadraticBezierCurve(let cp, let point):
|
||||
addQuadCurve(to: CGPoint(point), control: CGPoint(cp))
|
||||
case .ellipticalArcCurve(_, _, _, _, _, let point):
|
||||
guard let previousPoint else {
|
||||
addLine(to: CGPoint(point))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let curves = try command.convertToCubicBezierCurves(with: previousPoint)
|
||||
for curve in curves {
|
||||
addCommand(curve, from: from, to: to)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
addLine(to: CGPoint(point))
|
||||
}
|
||||
case .closePath:
|
||||
closeSubpath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
14
third-party/VectorPlus/Sources/CoreGraphics/Fill+CoreGraphics.swift
vendored
Normal file
14
third-party/VectorPlus/Sources/CoreGraphics/Fill+CoreGraphics.swift
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import Foundation
|
||||
import SwiftSVG
|
||||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
|
||||
public extension Fill.Rule {
|
||||
var cgFillRule: CGPathFillRule {
|
||||
switch self {
|
||||
case .evenOdd: .evenOdd
|
||||
case .nonZero: .winding
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
37
third-party/VectorPlus/Sources/CoreGraphics/SVG+CGPath.swift
vendored
Normal file
37
third-party/VectorPlus/Sources/CoreGraphics/SVG+CGPath.swift
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import Swift2D
|
||||
import SwiftSVG
|
||||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
|
||||
public extension SVG {
|
||||
func path(size: Size) -> CGPath {
|
||||
guard size.height > 0.0, size.width > 0.0 else {
|
||||
return CGMutablePath()
|
||||
}
|
||||
|
||||
guard let paths = try? subpaths() else {
|
||||
return CGMutablePath()
|
||||
}
|
||||
|
||||
let from = Rect(origin: .zero, size: originalSize)
|
||||
let to = Rect(origin: .zero, size: size)
|
||||
|
||||
let path = CGMutablePath()
|
||||
|
||||
for p in paths {
|
||||
let commands = (try? p.commands()) ?? []
|
||||
for (idx, command) in commands.enumerated() {
|
||||
let previous: Point? = if idx > 0 {
|
||||
commands[idx - 1].previousPoint
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
path.addCommand(command, from: from, to: to, previousPoint: previous)
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
#endif
|
||||
25
third-party/VectorPlus/Sources/CoreGraphics/Stroke+CoreGraphics.swift
vendored
Normal file
25
third-party/VectorPlus/Sources/CoreGraphics/Stroke+CoreGraphics.swift
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import Foundation
|
||||
import SwiftSVG
|
||||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
|
||||
public extension Stroke.LineCap {
|
||||
var cgLineCap: CGLineCap {
|
||||
switch self {
|
||||
case .butt: .butt
|
||||
case .round: .round
|
||||
case .square: .square
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Stroke.LineJoin {
|
||||
var cgLineJoin: CGLineJoin {
|
||||
switch self {
|
||||
case .bevel: .bevel
|
||||
case .arcs, .miter, .miterClip: .miter
|
||||
case .round: .round
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
29
third-party/VectorPlus/Sources/Fill+VectorPlus.swift
vendored
Normal file
29
third-party/VectorPlus/Sources/Fill+VectorPlus.swift
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import SwiftColor
|
||||
import SwiftSVG
|
||||
|
||||
public extension Fill {
|
||||
@available(*, deprecated, renamed: "pigment")
|
||||
var swiftColor: Pigment? { pigment }
|
||||
|
||||
var pigment: Pigment? {
|
||||
guard let color, !color.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let _color = Pigment(color)
|
||||
guard _color.alpha != 0.0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return _color
|
||||
}
|
||||
}
|
||||
|
||||
public extension Fill.Rule {
|
||||
var coreGraphicsDescription: String {
|
||||
switch self {
|
||||
case .evenOdd: ".evenOdd"
|
||||
case .nonZero: ".winding"
|
||||
}
|
||||
}
|
||||
}
|
||||
34
third-party/VectorPlus/Sources/Path+VectorPlus.swift
vendored
Normal file
34
third-party/VectorPlus/Sources/Path+VectorPlus.swift
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import Swift2D
|
||||
import SwiftSVG
|
||||
|
||||
public extension Path {
|
||||
func asCoreGraphicsDescription(variable: String = "path", originalSize: Size) throws -> String {
|
||||
var outputs: [String] = []
|
||||
let commands = (try? commands()) ?? []
|
||||
for (idx, command) in commands.enumerated() {
|
||||
let previous: Point? = if idx > 0 {
|
||||
commands[idx - 1].previousPoint
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
let method = command.coreGraphicsDescription(originalSize: originalSize, previousPoint: previous)
|
||||
let code = "\(variable)\(method)"
|
||||
outputs.append(code)
|
||||
}
|
||||
return outputs.joined(separator: "\n ")
|
||||
}
|
||||
}
|
||||
|
||||
public extension Path.Command {
|
||||
var previousPoint: Point {
|
||||
switch self {
|
||||
case .moveTo(let point): point
|
||||
case .lineTo(let point): point
|
||||
case .cubicBezierCurve(_, _, let point): point
|
||||
case .quadraticBezierCurve(_, let point): point
|
||||
case .ellipticalArcCurve(_, _, _, _, _, let point): point
|
||||
case .closePath: .zero
|
||||
}
|
||||
}
|
||||
}
|
||||
216
third-party/VectorPlus/Sources/Path.Command+VectorPlus.swift
vendored
Normal file
216
third-party/VectorPlus/Sources/Path.Command+VectorPlus.swift
vendored
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import Foundation
|
||||
import Swift2D
|
||||
import SwiftSVG
|
||||
|
||||
public extension Path.Command {
|
||||
/// Uses the _Power of Math_ to translate a commands controls/points from one `Rect` to another `Rect`.
|
||||
func translate(from: Rect, to: Rect) -> Path.Command {
|
||||
switch self {
|
||||
case .moveTo(let point):
|
||||
let _point = VectorPoint(point: point, in: from).translate(to: to)
|
||||
return .moveTo(point: _point)
|
||||
case .lineTo(let point):
|
||||
let _point = VectorPoint(point: point, in: from).translate(to: to)
|
||||
return .lineTo(point: _point)
|
||||
case .cubicBezierCurve(let cp1, let cp2, let point):
|
||||
let _cp1 = VectorPoint(point: cp1, in: from).translate(to: to)
|
||||
let _cp2 = VectorPoint(point: cp2, in: from).translate(to: to)
|
||||
let _point = VectorPoint(point: point, in: from).translate(to: to)
|
||||
return .cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point)
|
||||
case .quadraticBezierCurve(let cp, let point):
|
||||
let _cp = VectorPoint(point: cp, in: from).translate(to: to)
|
||||
let _point = VectorPoint(point: point, in: from).translate(to: to)
|
||||
return .quadraticBezierCurve(cp: _cp, point: _point)
|
||||
case .ellipticalArcCurve(let rx, let ry, let angle, let largeArc, let clockwise, let point):
|
||||
let _rx = rx * (from.size.maxRadius / to.size.minRadius)
|
||||
let _ry = ry * (from.size.maxRadius / to.size.minRadius)
|
||||
let _point = VectorPoint(point: point, in: from).translate(to: to)
|
||||
return .ellipticalArcCurve(rx: _rx, ry: _ry, angle: angle, largeArc: largeArc, clockwise: clockwise, point: _point)
|
||||
case .closePath:
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Path.Command {
|
||||
func coreGraphicsDescription(originalSize: Size, previousPoint: Point? = nil) -> String {
|
||||
let rect = Rect(origin: .zero, size: originalSize)
|
||||
|
||||
switch self {
|
||||
case .moveTo(let point):
|
||||
let _point = VectorPoint(point: point, in: rect)
|
||||
return ".move(to: \(_point.coreGraphicsDescription))"
|
||||
case .lineTo(let point):
|
||||
let _point = VectorPoint(point: point, in: rect)
|
||||
return ".addLine(to: \(_point.coreGraphicsDescription))"
|
||||
case .cubicBezierCurve(let cp1, let cp2, let point):
|
||||
let _cp1 = VectorPoint(point: cp1, in: rect)
|
||||
let _cp2 = VectorPoint(point: cp2, in: rect)
|
||||
let _point = VectorPoint(point: point, in: rect)
|
||||
return ".addCurve(to: \(_point.coreGraphicsDescription), control1: \(_cp1.coreGraphicsDescription), control2: \(_cp2.coreGraphicsDescription))"
|
||||
case .quadraticBezierCurve(let cp, let point):
|
||||
let _cp = VectorPoint(point: cp, in: rect)
|
||||
let _point = VectorPoint(point: point, in: rect)
|
||||
return ".addQuadCurve(to: \(_point.coreGraphicsDescription), control: \(_cp.coreGraphicsDescription))"
|
||||
case .ellipticalArcCurve(_, _, _, _, _, let point):
|
||||
guard let previousPoint else {
|
||||
return Path.Command.lineTo(point: point).coreGraphicsDescription(originalSize: originalSize)
|
||||
}
|
||||
|
||||
do {
|
||||
let curves = try convertToCubicBezierCurves(with: previousPoint)
|
||||
return curves.map { $0.coreGraphicsDescription(originalSize: originalSize) }.joined(separator: "\n")
|
||||
} catch {
|
||||
print(error)
|
||||
return Path.Command.lineTo(point: point).coreGraphicsDescription(originalSize: originalSize)
|
||||
}
|
||||
case .closePath:
|
||||
return ".closeSubpath()"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Path.Command {
|
||||
/// Converts an `.ellipticalArcCurve` into one or more `.cubicBezierCurve`s.
|
||||
/// https://github.com/colinmeinke/svg-arc-to-cubic-bezier/blob/master/src/index.js
|
||||
func convertToCubicBezierCurves(with previousPoint: Point) throws -> [Path.Command] {
|
||||
guard case let .ellipticalArcCurve(rx, ry, angle, largeArg, clockwise, point) = self else {
|
||||
throw Path.Command.Error.message("\(#function); Only .ellipticalArcCurve is allowed.")
|
||||
}
|
||||
|
||||
var curves: [Path.Command] = []
|
||||
|
||||
guard rx > 0.0, ry > 0.0 else {
|
||||
throw Path.Command.Error.message("\(#function); rx/ry must be greater than 0.0 (zero).")
|
||||
}
|
||||
|
||||
let sinφ = sin(angle * (.pi * 2.0) / 360.0)
|
||||
let cosφ = cos(angle * (.pi * 2.0) / 360.0)
|
||||
|
||||
let pxp = cosφ * (previousPoint.x - point.x) / 2 + sinφ * (previousPoint.y - point.y) / 2.0
|
||||
let pyp = -sinφ * (previousPoint.x - point.x) / 2 + cosφ * (previousPoint.y - point.y) / 2.0
|
||||
|
||||
guard pxp != 0.0, pyp != 0.0 else {
|
||||
throw Path.Command.Error.message("\(#function); math")
|
||||
}
|
||||
|
||||
var _rx = abs(rx)
|
||||
var _ry = abs(ry)
|
||||
|
||||
let λ = pow(pxp, 2.0) / pow(_rx, 2.0) + pow(pyp, 2.0) / pow(_ry, 2.0)
|
||||
|
||||
if λ > 1.0 {
|
||||
_rx *= sqrt(λ)
|
||||
_ry *= sqrt(λ)
|
||||
}
|
||||
|
||||
let _arcCenter = arcCenter(previousPoint: previousPoint, point: point, rx: _rx, ry: _ry, largeArc: largeArg, clockwise: clockwise, sinφ: sinφ, cosφ: cosφ, pxp: pxp, pyp: pyp)
|
||||
let center = _arcCenter.center
|
||||
var angle1 = _arcCenter.angle1
|
||||
var angle2 = _arcCenter.angle2
|
||||
|
||||
var ratio = abs(angle2) / ((.pi * 2.0) / 4.0)
|
||||
if abs(1.0 - ratio) < 0.0000001 {
|
||||
ratio = 1.0
|
||||
}
|
||||
|
||||
let segments = max(ceil(ratio), 1)
|
||||
|
||||
angle2 /= segments
|
||||
|
||||
var rawCurves: [(Point, Point, Point)] = []
|
||||
for _ in 0 ... Int(segments) {
|
||||
rawCurves.append(approximateUnitArc(angle1: angle1, angle2: angle2))
|
||||
angle1 += angle2
|
||||
}
|
||||
|
||||
for rawCurf in rawCurves {
|
||||
let _cp1 = mapToEllipse(point: rawCurf.0, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center)
|
||||
let _cp2 = mapToEllipse(point: rawCurf.1, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center)
|
||||
let _point = mapToEllipse(point: rawCurf.2, rx: _rx, ry: _ry, sinφ: sinφ, cosφ: cosφ, center: center)
|
||||
curves.append(.cubicBezierCurve(cp1: _cp1, cp2: _cp2, point: _point))
|
||||
}
|
||||
|
||||
return curves
|
||||
}
|
||||
}
|
||||
|
||||
private func arcCenter(previousPoint: Point, point: Point, rx: Double, ry: Double, largeArc: Bool, clockwise: Bool, sinφ: Double, cosφ: Double, pxp: Double, pyp: Double) ->
|
||||
(center: Point, angle1: Double, angle2: Double)
|
||||
{
|
||||
|
||||
let rxsq = pow(rx, 2.0)
|
||||
let rysq = pow(ry, 2.0)
|
||||
let pxpsq = pow(pxp, 2.0)
|
||||
let pypsq = pow(pyp, 2.0)
|
||||
|
||||
var radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq)
|
||||
if radicant < 0.0 {
|
||||
radicant = 0.0
|
||||
}
|
||||
|
||||
radicant /= (rxsq * pypsq) + (rysq * pxpsq)
|
||||
radicant = sqrt(radicant) * (largeArc == clockwise ? -1.0 : 1.0)
|
||||
|
||||
let centerxp = radicant * rx / ry * pyp
|
||||
let centeryp = radicant * -ry / rx * pxp
|
||||
|
||||
let centerx = cosφ * centerxp - sinφ * centeryp + (previousPoint.x + point.x) / 2.0
|
||||
let centery = sinφ * centerxp + cosφ * centeryp + (previousPoint.x + point.x) / 2.0
|
||||
|
||||
let vx1 = (pxp - centerxp) / rx
|
||||
let vy1 = (pyp - centeryp) / ry
|
||||
let vx2 = (-pxp - centerxp) / rx
|
||||
let vy2 = (-pyp - centeryp) / ry
|
||||
|
||||
let angle1 = vectorAngle(u: Point(x: 1, y: 0), v: Point(x: vx1, y: vy1))
|
||||
var angle2 = vectorAngle(u: Point(x: vx1, y: vy1), v: Point(x: vx2, y: vy2))
|
||||
|
||||
if clockwise == false, angle2 > 0.0 {
|
||||
angle2 -= (.pi * 2.0)
|
||||
} else if clockwise == true, angle2 < 0.0 {
|
||||
angle2 += (.pi * 2.0)
|
||||
}
|
||||
|
||||
return (Point(x: centerx, y: centery), angle1, angle2)
|
||||
}
|
||||
|
||||
private func vectorAngle(u: Point, v: Point) -> Double {
|
||||
let sign: Double = ((u.x * v.y - u.y * v.x) < 0.0) ? -1.0 : 1.0
|
||||
var dot = u.x * v.x + u.y * v.y
|
||||
if dot > 1.0 {
|
||||
dot = 1.0
|
||||
} else if dot < -1.0 {
|
||||
dot = -1.0
|
||||
}
|
||||
|
||||
return sign * acos(dot)
|
||||
}
|
||||
|
||||
private func approximateUnitArc(angle1: Double, angle2: Double) -> (Point, Point, Point) {
|
||||
// If 90 degree circular arc, use a constant
|
||||
// as derived from http://spencermortensen.com/articles/bezier-circle
|
||||
let a: Double = switch angle2 {
|
||||
case 1.5707963267948966:
|
||||
0.551915024494
|
||||
case -1.5707963267948966:
|
||||
-0.551915024494
|
||||
default:
|
||||
4.0 / 3.0 * tan(angle2 / 4.0)
|
||||
}
|
||||
|
||||
let x1 = cos(angle1)
|
||||
let y1 = sin(angle1)
|
||||
let x2 = cos(angle1 + angle2)
|
||||
let y2 = sin(angle1 + angle2)
|
||||
|
||||
return (Point(x: x1 - y1 * a, y: y1 + x1 * a), Point(x: x2 + y2 * a, y: y2 - x2 * 1), Point(x: x2, y: y2))
|
||||
}
|
||||
|
||||
private func mapToEllipse(point: Point, rx: Double, ry: Double, sinφ: Double, cosφ: Double, center: Point) -> Point {
|
||||
let x = point.x * rx
|
||||
let y = point.y * ry
|
||||
let xp = cosφ * x - sinφ * y
|
||||
let yp = sinφ * x + cosφ * y
|
||||
return Point(x: xp + center.x, y: yp + center.y)
|
||||
}
|
||||
7
third-party/VectorPlus/Sources/Pigment+VectorPlus.swift
vendored
Normal file
7
third-party/VectorPlus/Sources/Pigment+VectorPlus.swift
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import SwiftColor
|
||||
|
||||
public extension Pigment {
|
||||
var coreGraphicsDescription: String {
|
||||
"CGColor(srgbRed: \(red), green: \(green), blue: \(blue), alpha: \(alpha))"
|
||||
}
|
||||
}
|
||||
7
third-party/VectorPlus/Sources/Point+VectorPlus.swift
vendored
Normal file
7
third-party/VectorPlus/Sources/Point+VectorPlus.swift
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Swift2D
|
||||
|
||||
public extension Point {
|
||||
var coreGraphicsDescription: String {
|
||||
"CGPoint(x: \(x), y: \(y))"
|
||||
}
|
||||
}
|
||||
7
third-party/VectorPlus/Sources/Rect+VectorPlus.swift
vendored
Normal file
7
third-party/VectorPlus/Sources/Rect+VectorPlus.swift
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Swift2D
|
||||
|
||||
public extension Rect {
|
||||
var coreGraphicsDescription: String {
|
||||
"CGRect(origin: \(origin.coreGraphicsDescription), size: \(size.coreGraphicsDescription))"
|
||||
}
|
||||
}
|
||||
324
third-party/VectorPlus/Sources/SVG+AppleSymbols.swift
vendored
Normal file
324
third-party/VectorPlus/Sources/SVG+AppleSymbols.swift
vendored
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
import Foundation
|
||||
import Swift2D
|
||||
import SwiftSVG
|
||||
import XMLCoder
|
||||
|
||||
public extension SVG {
|
||||
|
||||
static func encodeDocument(_ document: SVG, encoder: XMLEncoder = XMLEncoder()) throws -> Data {
|
||||
let rootAttributes: [String: String] = [
|
||||
"version": "1.1",
|
||||
"xmlns": "http://www.w3.org/2000/svg",
|
||||
"xmlns:xlink": "http://www.w3.org/1999/xlink",
|
||||
]
|
||||
let header = XMLHeader(version: 1.0, encoding: "UTF-8", standalone: nil)
|
||||
return try encoder.encode(document, withRootKey: "svg", rootAttributes: rootAttributes, header: header)
|
||||
}
|
||||
|
||||
static func appleSymbols(path: Path, in rect: Rect) throws -> SVG {
|
||||
var document = SVG(width: 3300, height: 2200)
|
||||
document.groups = try [.appleSymbolsNotes, .appleSymbolsGuides, .appleSymbols(path: path, in: rect)]
|
||||
return document
|
||||
}
|
||||
}
|
||||
|
||||
public extension Group {
|
||||
static var appleSymbolsNotes: Group {
|
||||
var group = Group()
|
||||
|
||||
group.id = "Notes"
|
||||
group.rectangles = []
|
||||
group.lines = []
|
||||
group.texts = []
|
||||
group.groups = []
|
||||
|
||||
var artboard = Rectangle(x: 0, y: 0, width: 3300, height: 2200)
|
||||
artboard.id = "artboard"
|
||||
artboard.style = "fill:white;opacity:1"
|
||||
group.rectangles?.append(artboard)
|
||||
|
||||
var topLine = Line(x1: 263, y1: 292, x2: 3036, y2: 292)
|
||||
topLine.style = "fill:none;stroke:black;opacity:1;stroke-width:0.5;"
|
||||
group.lines?.append(topLine)
|
||||
|
||||
var bottomLine = Line(x1: 263, y1: 1903, x2: 3036, y2: 1903)
|
||||
bottomLine.style = "fill:none;stroke:black;opacity:1;stroke-width:0.5;"
|
||||
group.lines?.append(bottomLine)
|
||||
|
||||
var text = Text()
|
||||
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
|
||||
text.transform = "matrix(1 0 0 1 263 322)"
|
||||
text.value = "Weight/Scale Variations"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;text-anchor:middle"
|
||||
text.transform = "matrix(1 0 0 1 559.711 322)"
|
||||
text.value = "Ultralight"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 856.422 322)"
|
||||
text.value = "Thin"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 1153.13 322)"
|
||||
text.value = "Light"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 1449.84 322)"
|
||||
text.value = "Regular"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 1746.56 322)"
|
||||
text.value = "Medium"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 2043.27 322)"
|
||||
text.value = "Semibold"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 2339.98 322)"
|
||||
text.value = "Bold"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 2636.69 322)"
|
||||
text.value = "Heavy"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 2933.4 322)"
|
||||
text.value = "Black"
|
||||
group.texts?.append(text)
|
||||
|
||||
var path = Path(data: "M 9.24805 0.830078 C 13.5547 0.830078 17.1387 -2.74414 17.1387 -7.05078 C 17.1387 -11.3574 13.5449 -14.9316 9.23828 -14.9316 C 4.94141 -14.9316 1.36719 -11.3574 1.36719 -7.05078 C 1.36719 -2.74414 4.95117 0.830078 9.24805 0.830078 Z M 9.24805 -0.654297 C 5.70312 -0.654297 2.87109 -3.49609 2.87109 -7.05078 C 2.87109 -10.6055 5.69336 -13.4473 9.23828 -13.4473 C 12.793 -13.4473 15.6348 -10.6055 15.6445 -7.05078 C 15.6543 -3.49609 12.8027 -0.654297 9.24805 -0.654297 Z M 9.22852 -3.42773 C 9.69727 -3.42773 9.9707 -3.74023 9.9707 -4.25781 L 9.9707 -6.31836 L 12.1973 -6.31836 C 12.6953 -6.31836 13.0371 -6.57227 13.0371 -7.04102 C 13.0371 -7.51953 12.7148 -7.7832 12.1973 -7.7832 L 9.9707 -7.7832 L 9.9707 -10.0098 C 9.9707 -10.5273 9.69727 -10.8496 9.22852 -10.8496 C 8.75977 -10.8496 8.50586 -10.5078 8.50586 -10.0098 L 8.50586 -7.7832 L 6.29883 -7.7832 C 5.78125 -7.7832 5.44922 -7.51953 5.44922 -7.04102 C 5.44922 -6.57227 5.80078 -6.31836 6.29883 -6.31836 L 8.50586 -6.31836 L 8.50586 -4.25781 C 8.50586 -3.75977 8.75977 -3.42773 9.22852 -3.42773 Z")
|
||||
var subGroup = Group("", path: path, transform: "matrix(1 0 0 1 263 1933)")
|
||||
group.groups?.append(subGroup)
|
||||
|
||||
path.data = "M 11.709 2.91016 C 17.1582 2.91016 21.6699 -1.60156 21.6699 -7.05078 C 21.6699 -12.4902 17.1484 -17.0117 11.6992 -17.0117 C 6.25977 -17.0117 1.74805 -12.4902 1.74805 -7.05078 C 1.74805 -1.60156 6.26953 2.91016 11.709 2.91016 Z M 11.709 1.25 C 7.09961 1.25 3.41797 -2.44141 3.41797 -7.05078 C 3.41797 -11.6504 7.08984 -15.3516 11.6992 -15.3516 C 16.3086 -15.3516 20 -11.6504 20.0098 -7.05078 C 20.0195 -2.44141 16.3184 1.25 11.709 1.25 Z M 11.6895 -2.41211 C 12.207 -2.41211 12.5195 -2.77344 12.5195 -3.33984 L 12.5195 -6.23047 L 15.5762 -6.23047 C 16.123 -6.23047 16.5039 -6.51367 16.5039 -7.03125 C 16.5039 -7.55859 16.1426 -7.86133 15.5762 -7.86133 L 12.5195 -7.86133 L 12.5195 -10.9277 C 12.5195 -11.5039 12.207 -11.8555 11.6895 -11.8555 C 11.1719 -11.8555 10.8789 -11.4844 10.8789 -10.9277 L 10.8789 -7.86133 L 7.83203 -7.86133 C 7.26562 -7.86133 6.89453 -7.55859 6.89453 -7.03125 C 6.89453 -6.51367 7.28516 -6.23047 7.83203 -6.23047 L 10.8789 -6.23047 L 10.8789 -3.33984 C 10.8789 -2.79297 11.1719 -2.41211 11.6895 -2.41211 Z"
|
||||
subGroup.paths = [path]
|
||||
subGroup.transform = "matrix(1 0 0 1 281.506 1933)"
|
||||
group.groups?.append(subGroup)
|
||||
|
||||
path.data = "M 14.9707 5.67383 C 21.9336 5.67383 27.6953 -0.078125 27.6953 -7.04102 C 27.6953 -14.0039 21.9238 -19.7559 14.9609 -19.7559 C 8.00781 -19.7559 2.25586 -14.0039 2.25586 -7.04102 C 2.25586 -0.078125 8.01758 5.67383 14.9707 5.67383 Z M 14.9707 3.85742 C 8.93555 3.85742 4.08203 -1.00586 4.08203 -7.04102 C 4.08203 -13.0762 8.92578 -17.9395 14.9609 -17.9395 C 21.0059 -17.9395 25.8594 -13.0762 25.8691 -7.04102 C 25.8789 -1.00586 21.0156 3.85742 14.9707 3.85742 Z M 14.9512 -1.06445 C 15.5176 -1.06445 15.8691 -1.45508 15.8691 -2.06055 L 15.8691 -6.13281 L 20.1074 -6.13281 C 20.6934 -6.13281 21.1133 -6.46484 21.1133 -7.02148 C 21.1133 -7.59766 20.7227 -7.93945 20.1074 -7.93945 L 15.8691 -7.93945 L 15.8691 -12.1875 C 15.8691 -12.8027 15.5176 -13.1934 14.9512 -13.1934 C 14.3848 -13.1934 14.0625 -12.7832 14.0625 -12.1875 L 14.0625 -7.93945 L 9.83398 -7.93945 C 9.21875 -7.93945 8.80859 -7.59766 8.80859 -7.02148 C 8.80859 -6.46484 9.23828 -6.13281 9.83398 -6.13281 L 14.0625 -6.13281 L 14.0625 -2.06055 C 14.0625 -1.47461 14.3848 -1.06445 14.9512 -1.06445 Z"
|
||||
subGroup.paths = [path]
|
||||
subGroup.transform = "matrix(1 0 0 1 304.924 1933)"
|
||||
group.groups?.append(subGroup)
|
||||
|
||||
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
|
||||
text.transform = "matrix(1 0 0 1 263 1953)"
|
||||
text.value = "Design Variations"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.style = "none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
|
||||
text.transform = "matrix(1 0 0 1 263 1971)"
|
||||
text.value = "Symbols are supported in up to nine weights and three scales."
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 263 1989)"
|
||||
text.value = "For optimal layout with text and other symbols, vertically align"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 263 2007)"
|
||||
text.value = "symbols with the adjacent text."
|
||||
group.texts?.append(text)
|
||||
|
||||
var rect = Rectangle(x: 776, y: 1919, width: 3, height: 14)
|
||||
rect.style = "fill:#00AEEF;stroke:none;opacity:0.4;"
|
||||
group.rectangles?.append(rect)
|
||||
|
||||
path.data = "M 10.5273 0 L 12.373 0 L 7.17773 -14.0918 L 5.43945 -14.0918 L 0.244141 0 L 2.08984 0 L 3.50586 -4.0332 L 9.11133 -4.0332 Z M 6.2793 -11.9531 L 6.33789 -11.9531 L 8.59375 -5.52734 L 4.02344 -5.52734 Z"
|
||||
subGroup.paths = [path]
|
||||
subGroup.transform = "matrix(1 0 0 1 779 1933)"
|
||||
group.groups?.append(subGroup)
|
||||
|
||||
rect.x = 791.617
|
||||
group.rectangles?.append(rect)
|
||||
|
||||
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
|
||||
text.transform = "matrix(1 0 0 1 776 1953)"
|
||||
text.value = "Margins"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
|
||||
text.transform = "matrix(1 0 0 1 776 1971)"
|
||||
text.value = "Leading and trailing margins on the left and right side of each symbol"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 776 1989)"
|
||||
text.value = "can be adjusted by modifying the width of the blue rectangles."
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 776 2007)"
|
||||
text.value = "Modifications are automatically applied proportionally to all"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 776 2025)"
|
||||
text.value = "scales and weights."
|
||||
group.texts?.append(text)
|
||||
|
||||
path.data = "M 2.83203 3.11523 L 4.375 4.6582 C 5.22461 5.48828 6.19141 5.41992 7.06055 4.46289 L 17.2754 -6.66016 C 17.7051 -6.36719 18.0957 -6.37695 18.5645 -6.47461 L 19.6094 -6.68945 L 20.3027 -5.99609 L 20.2539 -5.47852 C 20.1855 -4.95117 20.3516 -4.53125 20.8496 -4.0332 L 21.6602 -3.22266 C 22.168 -2.71484 22.8223 -2.68555 23.3008 -3.16406 L 26.5527 -6.41602 C 27.0312 -6.89453 27.0117 -7.54883 26.5039 -8.05664 L 25.6836 -8.87695 C 25.1855 -9.375 24.7754 -9.55078 24.2383 -9.47266 L 23.7109 -9.41406 L 23.0566 -10.0781 L 23.3398 -11.2207 C 23.4863 -11.7871 23.3398 -12.2559 22.7148 -12.8613 L 20.3027 -15.2539 C 16.7578 -18.7793 12.2266 -18.6719 9.11133 -15.5371 C 8.69141 -15.1074 8.64258 -14.5215 8.91602 -14.0918 C 9.15039 -13.7207 9.62891 -13.4961 10.2734 -13.6621 C 11.7871 -14.043 13.3008 -13.9258 14.7852 -12.9199 L 14.1602 -11.3379 C 13.9258 -10.752 13.9453 -10.2734 14.1797 -9.83398 L 3.01758 0.439453 C 2.08008 1.30859 1.97266 2.25586 2.83203 3.11523 Z M 10.6738 -15.1465 C 13.3398 -17.1387 16.6504 -16.8262 19.0527 -14.4141 L 21.6797 -11.8066 C 21.9141 -11.5723 21.9434 -11.3867 21.8848 -11.0938 L 21.5039 -9.53125 L 23.0762 -7.95898 L 24.043 -8.04688 C 24.3262 -8.07617 24.4141 -8.05664 24.6387 -7.83203 L 25.2637 -7.20703 L 22.5098 -4.46289 L 21.8848 -5.07812 C 21.6602 -5.30273 21.6406 -5.40039 21.6699 -5.68359 L 21.7578 -6.64062 L 20.1953 -8.20312 L 18.5742 -7.89062 C 18.291 -7.83203 18.1445 -7.83203 17.9102 -8.07617 L 15.7324 -10.2539 C 15.5078 -10.4883 15.4785 -10.625 15.6055 -10.9473 L 16.5527 -13.2227 C 14.9512 -14.7559 12.8418 -15.6055 10.8008 -14.9512 C 10.7129 -14.9219 10.6445 -14.9414 10.6152 -14.9805 C 10.5859 -15.0293 10.5859 -15.0781 10.6738 -15.1465 Z M 4.10156 2.41211 C 3.61328 1.91406 3.78906 1.61133 4.12109 1.30859 L 15.0781 -8.80859 L 16.3086 -7.57812 L 6.15234 3.34961 C 5.84961 3.68164 5.46875 3.7793 5.06836 3.37891 Z"
|
||||
subGroup.paths = [path]
|
||||
subGroup.transform = "matrix(1 0 0 1 1289 1933)"
|
||||
group.groups?.append(subGroup)
|
||||
|
||||
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;font-weight:bold;"
|
||||
text.transform = "matrix(1 0 0 1 1289 1953)"
|
||||
text.value = "Exporting"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
|
||||
text.transform = "matrix(1 0 0 1 1289 1971)"
|
||||
text.value = "Symbols should be outlined when exporting to ensure the"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 1289 1989)"
|
||||
text.value = "design is preserved when submitting to Xcode."
|
||||
group.texts?.append(text)
|
||||
|
||||
text.id = "template-version"
|
||||
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;text-anchor:end;"
|
||||
text.transform = "matrix(1 0 0 1 3036 1933)"
|
||||
text.value = "Template v.2.0"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.id = nil
|
||||
text.transform = "matrix(1 0 0 1 3036 1969)"
|
||||
text.value = "Typeset at 100 points"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.style = "stroke:none;fill:black;font-family:-apple-system,\"SF Pro Display\",\"SF Pro Text\",Helvetica,sans-serif;"
|
||||
text.transform = "matrix(1 0 0 1 263 726)"
|
||||
text.value = "Small"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 263 1156)"
|
||||
text.value = "Medium"
|
||||
group.texts?.append(text)
|
||||
|
||||
text.transform = "matrix(1 0 0 1 263 1586)"
|
||||
text.value = "Large"
|
||||
group.texts?.append(text)
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
static var appleSymbolsGuides: Group {
|
||||
var group = Group()
|
||||
|
||||
group.id = "Guides"
|
||||
group.groups = []
|
||||
group.lines = []
|
||||
|
||||
var hRef = Group()
|
||||
hRef.id = "H-reference"
|
||||
hRef.style = "fill:#27AAE1;stroke:none;"
|
||||
hRef.transform = "matrix(1 0 0 1 339 696)"
|
||||
hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")]
|
||||
group.groups?.append(hRef)
|
||||
|
||||
var baseline = Line(x1: 263, y1: 696, x2: 3036, y2: 696)
|
||||
baseline.id = "Baseline-S"
|
||||
baseline.style = "fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.577;"
|
||||
group.lines?.append(baseline)
|
||||
|
||||
var capline = Line(x1: 263, y1: 625.541, x2: 3036, y2: 625.541)
|
||||
capline.id = "Capline-S"
|
||||
capline.style = "fill:none;stroke:#27AAE1;opacity:1;stroke-width:0.577;"
|
||||
group.lines?.append(capline)
|
||||
|
||||
hRef.transform = "matrix(1 0 0 1 339 1126)"
|
||||
hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")]
|
||||
group.groups?.append(hRef)
|
||||
|
||||
baseline.id = "Baseline-M"
|
||||
baseline.y1 = 1126
|
||||
baseline.y2 = 1126
|
||||
group.lines?.append(baseline)
|
||||
|
||||
capline.id = "Capline-M"
|
||||
capline.y1 = 1055.54
|
||||
capline.y2 = 1055.54
|
||||
group.lines?.append(capline)
|
||||
|
||||
hRef.transform = "matrix(1 0 0 1 339 1556)"
|
||||
hRef.paths = [Path(data: "M 54.9316 0 L 57.666 0 L 30.5664 -70.459 L 28.0762 -70.459 L 0.976562 0 L 3.66211 0 L 12.9395 -24.4629 L 45.7031 -24.4629 Z M 29.1992 -67.0898 L 29.4434 -67.0898 L 44.8242 -26.709 L 13.8184 -26.709 Z")]
|
||||
group.groups?.append(hRef)
|
||||
|
||||
baseline.id = "Baseline-L"
|
||||
baseline.y1 = 1556
|
||||
baseline.y2 = 1556
|
||||
group.lines?.append(baseline)
|
||||
|
||||
capline.id = "Capline-L"
|
||||
capline.y1 = 1485.54
|
||||
capline.y2 = 1485.54
|
||||
group.lines?.append(capline)
|
||||
|
||||
var margin = Line(x1: 1399.72, y1: 1030.79, x2: 1399.72, y2: 1150.12)
|
||||
margin.id = "left-margin"
|
||||
margin.style = "fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
|
||||
group.lines?.append(margin)
|
||||
|
||||
margin = Line(x1: 1499.97, y1: 1030.79, x2: 1499.97, y2: 1150.12)
|
||||
margin.id = "right-margin"
|
||||
margin.style = "fill:none;stroke:#00AEEF;stroke-width:0.5;opacity:1.0;"
|
||||
group.lines?.append(margin)
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
static func appleSymbols(path: Path, in rect: Rect) throws -> Group {
|
||||
var group = Group()
|
||||
|
||||
group.id = "Symbols"
|
||||
|
||||
let translations: [(name: String, size: Float, center: Point)] = [
|
||||
("Ultralight-S", 0.75, .init(x: 559.0, y: 661.0)),
|
||||
("Thin-S", 0.76, .init(x: 857.0, y: 661.0)),
|
||||
("Light-S", 0.78, .init(x: 1153.0, y: 661.0)),
|
||||
("Regular-S", 0.79, .init(x: 1449.5, y: 661.0)),
|
||||
("Medium-S", 0.80, .init(x: 1747.0, y: 661.0)),
|
||||
("Semibold-S", 0.81, .init(x: 2043.0, y: 661.0)),
|
||||
("Bold-S", 0.82, .init(x: 2340.0, y: 661.0)),
|
||||
("Heavy-S", 0.85, .init(x: 2636.5, y: 661.0)),
|
||||
("Black-S", 0.86, .init(x: 2933.0, y: 661.0)),
|
||||
("Ultralight-M", 0.95, .init(x: 559.0, y: 1091.0)),
|
||||
("Thin-M", 0.96, .init(x: 857.0, y: 1091.0)),
|
||||
("Light-M", 0.98, .init(x: 1153.0, y: 1091.0)),
|
||||
("Regular-M", 1.00, .init(x: 1449.5, y: 1091.0)),
|
||||
("Medium-M", 1.02, .init(x: 1747.0, y: 1091.0)),
|
||||
("Semibold-M", 1.03, .init(x: 2043.0, y: 1091.0)),
|
||||
("Bold-M", 1.05, .init(x: 2340.0, y: 1091.0)),
|
||||
("Heavy-M", 1.07, .init(x: 2636.5, y: 1091.0)),
|
||||
("Black-M", 1.10, .init(x: 2933.0, y: 1091.0)),
|
||||
("Ultralight-L", 1.22, .init(x: 559.0, y: 1521.0)),
|
||||
("Thin-L", 1.24, .init(x: 857.0, y: 1521.0)),
|
||||
("Light-L", 1.26, .init(x: 1153.0, y: 1521.0)),
|
||||
("Regular-L", 1.28, .init(x: 1449.5, y: 1521.0)),
|
||||
("Medium-L", 1.30, .init(x: 1747.0, y: 1521.0)),
|
||||
("Semibold-L", 1.31, .init(x: 2043.0, y: 1521.0)),
|
||||
("Bold-L", 1.33, .init(x: 2340.0, y: 1521.0)),
|
||||
("Heavy-L", 1.36, .init(x: 2636.5, y: 1521.0)),
|
||||
("Black-L", 1.39, .init(x: 2933.0, y: 1521.0)),
|
||||
]
|
||||
|
||||
group.groups = try translations.map { symbol -> Group in
|
||||
let size = Size(width: 100.0 * symbol.size, height: 100.0 * symbol.size)
|
||||
let to = Rect(origin: .zero, size: size)
|
||||
let commands = try path.commands().map { $0.translate(from: rect, to: to) }
|
||||
let p = Path(commands: commands)
|
||||
let matrixOrigin = Point(x: symbol.center.x - size.width / 2.0, y: symbol.center.y - size.height / 2.0)
|
||||
let matrix: Transformation = .matrix(a: 1, b: 0, c: 0, d: 1, e: matrixOrigin.x, f: matrixOrigin.y)
|
||||
return Group(symbol.name, path: p, transform: matrix.description)
|
||||
}
|
||||
|
||||
return group
|
||||
}
|
||||
}
|
||||
|
||||
private extension Group {
|
||||
init(_ id: String, path: Path, transform: String) {
|
||||
self.init()
|
||||
self.id = id
|
||||
self.transform = transform
|
||||
paths = [path]
|
||||
}
|
||||
}
|
||||
56
third-party/VectorPlus/Sources/SVG+Template.swift
vendored
Normal file
56
third-party/VectorPlus/Sources/SVG+Template.swift
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import Foundation
|
||||
import Swift2D
|
||||
import SwiftSVG
|
||||
|
||||
public extension SVG {
|
||||
func asImageViewSubclass() throws -> String {
|
||||
let instructions = try asCoreGraphicsDescription()
|
||||
let renders = try asCGContextDescription()
|
||||
|
||||
return imageViewSubclassTemplate
|
||||
.replacingOccurrences(of: "{{name}}", with: name)
|
||||
.replacingOccurrences(of: "{{width}}", with: String(format: "%.1f", originalSize.width))
|
||||
.replacingOccurrences(of: "{{height}}", with: String(format: "%.1f", originalSize.height))
|
||||
.replacingOccurrences(of: "{{instructions}}", with: instructions)
|
||||
.replacingOccurrences(of: "{{render}}", with: renders)
|
||||
}
|
||||
}
|
||||
|
||||
private extension SVG {
|
||||
func asCoreGraphicsDescription(variable: String = "path") throws -> String {
|
||||
try subpaths().map { try $0.asCoreGraphicsDescription(variable: variable, originalSize: originalSize) }.joined(separator: "\n ")
|
||||
}
|
||||
|
||||
func asCGContextDescription() throws -> String {
|
||||
var outputs: [String] = []
|
||||
|
||||
let paths = try subpaths()
|
||||
try paths.forEach { path in
|
||||
let instructions = try path.asCoreGraphicsDescription(variable: "path", originalSize: originalSize)
|
||||
let fillColor = path.fill?.pigment?.coreGraphicsDescription ?? "nil"
|
||||
let fillOpacity = (path.fillOpacity != nil) ? "\(path.fillOpacity!)" : "nil"
|
||||
let fillRule = (path.fillRule ?? .nonZero).coreGraphicsDescription
|
||||
let strokeColor = path.stroke?.pigment?.coreGraphicsDescription ?? "nil"
|
||||
let strokeOpacity = (path.strokeOpacity != nil) ? "\(path.strokeOpacity!)" : "nil"
|
||||
let strokeWidth = (path.strokeWidth != nil) ? "\(path.strokeWidth!) * (size.width / width)" : "nil"
|
||||
let strokeLineCap = (path.strokeLineCap != nil) ? "\(path.strokeLineCap!.coreGraphicsDescription)" : "nil"
|
||||
let strokeLineJoin = (path.strokeLineJoin != nil) ? "\(path.strokeLineJoin!.coreGraphicsDescription)" : "nil"
|
||||
let strokeMiterLimit = (path.strokeMiterLimit != nil) ? "\(path.strokeMiterLimit!)" : "nil"
|
||||
|
||||
outputs.append(contextTemplate
|
||||
.replacingOccurrences(of: "{{instructions}}", with: instructions)
|
||||
.replacingOccurrences(of: "{{fillColor}}", with: fillColor)
|
||||
.replacingOccurrences(of: "{{fillOpacity}}", with: fillOpacity)
|
||||
.replacingOccurrences(of: "{{fillRule}}", with: fillRule)
|
||||
.replacingOccurrences(of: "{{strokeColor}}", with: strokeColor)
|
||||
.replacingOccurrences(of: "{{strokeOpacity}}", with: strokeOpacity)
|
||||
.replacingOccurrences(of: "{{strokeWidth}}", with: strokeWidth)
|
||||
.replacingOccurrences(of: "{{strokeLineCap}}", with: strokeLineCap)
|
||||
.replacingOccurrences(of: "{{strokeLineJoin}}", with: strokeLineJoin)
|
||||
.replacingOccurrences(of: "{{strokeMiterLimit}}", with: strokeMiterLimit)
|
||||
)
|
||||
}
|
||||
|
||||
return outputs.joined(separator: "\n ")
|
||||
}
|
||||
}
|
||||
7
third-party/VectorPlus/Sources/Size+VectorPlus.swift
vendored
Normal file
7
third-party/VectorPlus/Sources/Size+VectorPlus.swift
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Swift2D
|
||||
|
||||
public extension Size {
|
||||
var coreGraphicsDescription: String {
|
||||
"CGSize(width: \(width), height: \(height))"
|
||||
}
|
||||
}
|
||||
40
third-party/VectorPlus/Sources/Stroke+VectorPlus.swift
vendored
Normal file
40
third-party/VectorPlus/Sources/Stroke+VectorPlus.swift
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import SwiftColor
|
||||
import SwiftSVG
|
||||
|
||||
public extension Stroke {
|
||||
@available(*, deprecated, renamed: "pigment")
|
||||
var swiftColor: Pigment? { pigment }
|
||||
|
||||
var pigment: Pigment? {
|
||||
guard let color, !color.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let _color = Pigment(color)
|
||||
guard _color.alpha != 0.0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return _color
|
||||
}
|
||||
}
|
||||
|
||||
public extension Stroke.LineCap {
|
||||
var coreGraphicsDescription: String {
|
||||
switch self {
|
||||
case .butt: ".butt"
|
||||
case .round: ".round"
|
||||
case .square: ".square"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Stroke.LineJoin {
|
||||
var coreGraphicsDescription: String {
|
||||
switch self {
|
||||
case .bevel: ".bevel"
|
||||
case .arcs, .miter, .miterClip: ".miter"
|
||||
case .round: ".round"
|
||||
}
|
||||
}
|
||||
}
|
||||
177
third-party/VectorPlus/Sources/Template+UIImageView.swift
vendored
Normal file
177
third-party/VectorPlus/Sources/Template+UIImageView.swift
vendored
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
let imageViewSubclassTemplate: String = """
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
|
||||
@IBDesignable
|
||||
public class {{name}}: UIImageView {
|
||||
|
||||
public static let width: CGFloat = {{width}}
|
||||
public static let height: CGFloat = {{height}}
|
||||
public let width: CGFloat = {{width}}
|
||||
public let height: CGFloat = {{height}}
|
||||
|
||||
public var widthToHeightAspectRatio: CGFloat {
|
||||
guard width != .nan, width > 0.0 else {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
guard height != .nan, height > 0.0 else {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
return width / height
|
||||
}
|
||||
|
||||
public var heightToWidthAspectRatio: CGFloat {
|
||||
guard height != .nan, height > 0.0 else {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
guard width != .nan, width > 0.0 else {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
return height / width
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
updateSubviews()
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
updateSubviews()
|
||||
}
|
||||
|
||||
public override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
public override var bounds: CGRect {
|
||||
didSet {
|
||||
updateSubviews()
|
||||
}
|
||||
}
|
||||
|
||||
public override func prepareForInterfaceBuilder() {
|
||||
super.prepareForInterfaceBuilder()
|
||||
updateSubviews()
|
||||
}
|
||||
|
||||
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
updateSubviews()
|
||||
}
|
||||
|
||||
public func updateSubviews() {
|
||||
image = Self.image(size: bounds.size)
|
||||
}
|
||||
|
||||
public static func path(size: CGSize) -> CGPath {
|
||||
guard size.height > 0.0 && size.width > 0.0 else {
|
||||
return CGMutablePath()
|
||||
}
|
||||
|
||||
let radius = max(size.width / 2.0, size.height / 2.0)
|
||||
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
|
||||
let path = CGMutablePath()
|
||||
{{instructions}}
|
||||
return path
|
||||
}
|
||||
|
||||
public static func image(size: CGSize) -> UIImage? {
|
||||
guard size.height > 0.0 && size.width > 0.0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let radius = max(size.width / 2.0, size.height / 2.0)
|
||||
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
|
||||
defer {
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||
|
||||
guard let context = UIGraphicsGetCurrentContext() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
{{render}}
|
||||
|
||||
return UIGraphicsGetImageFromCurrentImageContext()
|
||||
}
|
||||
|
||||
private static func radians(_ degree: Float) -> CGFloat {
|
||||
return CGFloat(degree) * (.pi / CGFloat(180))
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGContext {
|
||||
func rendering(_ block: (CGContext) -> Void) {
|
||||
block(self)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
"""
|
||||
|
||||
let contextTemplate: String = """
|
||||
context.rendering { (ctx) in
|
||||
ctx.saveGState()
|
||||
|
||||
let path = CGMutablePath()
|
||||
{{instructions}}
|
||||
|
||||
let defaultColor: CGColor = CGColor(srgbRed: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
|
||||
let pathFillColor: CGColor? = {{fillColor}}
|
||||
let pathFillOpacity: CGFloat? = {{fillOpacity}}
|
||||
let pathFillRule: CGPathFillRule = {{fillRule}}
|
||||
let pathStrokeColor: CGColor? = {{strokeColor}}
|
||||
let pathStrokeOpacity: CGFloat? = {{strokeOpacity}}
|
||||
let pathStrokeWidth: CGFloat? = {{strokeWidth}}
|
||||
let pathStrokeLineCap: CGLineCap? = {{strokeLineCap}}
|
||||
let pathStrokeLineJoin: CGLineJoin? = {{strokeLineJoin}}
|
||||
let pathStrokeMiterLimit: CGFloat? = {{strokeMiterLimit}}
|
||||
|
||||
if pathFillColor != nil && pathFillOpacity != nil {
|
||||
let opacity = pathFillOpacity ?? 1.0
|
||||
let color = (pathFillColor ?? defaultColor).copy(alpha: opacity) ?? defaultColor
|
||||
|
||||
ctx.setFillColor(color)
|
||||
ctx.addPath(path)
|
||||
ctx.fillPath(using: pathFillRule)
|
||||
}
|
||||
|
||||
if pathStrokeColor != nil && pathStrokeOpacity != nil {
|
||||
let opacity = pathStrokeOpacity ?? 1.0
|
||||
let color = (pathStrokeColor ?? defaultColor).copy(alpha: opacity) ?? defaultColor
|
||||
let lineWidth = pathStrokeWidth ?? 1.0
|
||||
|
||||
ctx.setLineWidth(lineWidth)
|
||||
ctx.setStrokeColor(color)
|
||||
if let lineCap = pathStrokeLineCap {
|
||||
ctx.setLineCap(lineCap)
|
||||
}
|
||||
if let lineJoin = pathStrokeLineJoin {
|
||||
ctx.setLineJoin(lineJoin)
|
||||
if let miterLimit = pathStrokeMiterLimit, lineJoin == .miter {
|
||||
ctx.setMiterLimit(miterLimit)
|
||||
}
|
||||
}
|
||||
ctx.addPath(path)
|
||||
ctx.strokePath()
|
||||
}
|
||||
|
||||
if (pathFillColor == nil && pathFillOpacity == nil) && (pathStrokeColor == nil && pathStrokeOpacity == nil) {
|
||||
ctx.setFillColor(defaultColor)
|
||||
ctx.addPath(path)
|
||||
ctx.fillPath(using: pathFillRule)
|
||||
}
|
||||
|
||||
ctx.restoreGState()
|
||||
}
|
||||
"""
|
||||
43
third-party/VectorPlus/Sources/UIKit/SVG+UIImage.swift
vendored
Normal file
43
third-party/VectorPlus/Sources/UIKit/SVG+UIImage.swift
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#if canImport(UIKit)
|
||||
import Swift2D
|
||||
import SwiftSVG
|
||||
import UIKit
|
||||
|
||||
public extension SVG {
|
||||
func uiImage(size: Size) -> UIImage? {
|
||||
guard size.height > 0.0, size.width > 0.0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let from = Rect(origin: .zero, size: originalSize)
|
||||
let to = Rect(origin: .zero, size: size)
|
||||
|
||||
let paths: [Path]
|
||||
do {
|
||||
paths = try subpaths()
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer {
|
||||
UIGraphicsEndImageContext()
|
||||
}
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(CGSize(size), false, 0.0)
|
||||
|
||||
guard let context = UIGraphicsGetCurrentContext() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
for path in paths {
|
||||
try? context.render(path: path, from: from, to: to)
|
||||
}
|
||||
|
||||
return UIGraphicsGetImageFromCurrentImageContext()
|
||||
}
|
||||
|
||||
func pngData(size: Size) -> Data? {
|
||||
uiImage(size: size)?.pngData()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue