mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
feat(apple): port the zen, drummer, and peekaboo claw stances (#112570)
* feat(apple): add rare working claw stances Add zen, drummer, and peekaboo animations to the shared iOS and macOS working indicator, with deterministic stance and pose coverage. * chore(i18n): refresh native source anchors
This commit is contained in:
committed by
GitHub
parent
f1cfaa94a0
commit
5610f24fc8
@@ -40987,7 +40987,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 40,
|
||||
"line": 46,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Shelling",
|
||||
"surface": "apple",
|
||||
@@ -40995,7 +40995,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 41,
|
||||
"line": 47,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Scuttling",
|
||||
"surface": "apple",
|
||||
@@ -41003,7 +41003,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 42,
|
||||
"line": 48,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Clawing",
|
||||
"surface": "apple",
|
||||
@@ -41011,7 +41011,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 43,
|
||||
"line": 49,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Pinching",
|
||||
"surface": "apple",
|
||||
@@ -41019,7 +41019,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 44,
|
||||
"line": 50,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Molting",
|
||||
"surface": "apple",
|
||||
@@ -41027,7 +41027,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 45,
|
||||
"line": 51,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Bubbling",
|
||||
"surface": "apple",
|
||||
@@ -41035,7 +41035,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 46,
|
||||
"line": 52,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Tiding",
|
||||
"surface": "apple",
|
||||
@@ -41043,7 +41043,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 47,
|
||||
"line": 53,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Reefing",
|
||||
"surface": "apple",
|
||||
@@ -41051,7 +41051,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 48,
|
||||
"line": 54,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Cracking",
|
||||
"surface": "apple",
|
||||
@@ -41059,7 +41059,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 49,
|
||||
"line": 55,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Sifting",
|
||||
"surface": "apple",
|
||||
@@ -41067,7 +41067,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 50,
|
||||
"line": 56,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Brining",
|
||||
"surface": "apple",
|
||||
@@ -41075,7 +41075,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 51,
|
||||
"line": 57,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Nautiling",
|
||||
"surface": "apple",
|
||||
@@ -41083,7 +41083,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 52,
|
||||
"line": 58,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Krilling",
|
||||
"surface": "apple",
|
||||
@@ -41091,7 +41091,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 53,
|
||||
"line": 59,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Barnacling",
|
||||
"surface": "apple",
|
||||
@@ -41099,7 +41099,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 54,
|
||||
"line": 60,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Lobstering",
|
||||
"surface": "apple",
|
||||
@@ -41107,7 +41107,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 55,
|
||||
"line": 61,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Tidepooling",
|
||||
"surface": "apple",
|
||||
@@ -41115,7 +41115,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 56,
|
||||
"line": 62,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Pearling",
|
||||
"surface": "apple",
|
||||
@@ -41123,7 +41123,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 57,
|
||||
"line": 63,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Snapping",
|
||||
"surface": "apple",
|
||||
@@ -41131,7 +41131,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 58,
|
||||
"line": 64,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Surfacing",
|
||||
"surface": "apple",
|
||||
@@ -41139,7 +41139,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 129,
|
||||
"line": 135,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "Done in %@",
|
||||
"surface": "apple",
|
||||
@@ -41147,7 +41147,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 139,
|
||||
"line": 145,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "1 token",
|
||||
"surface": "apple",
|
||||
@@ -41155,7 +41155,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 142,
|
||||
"line": 148,
|
||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingProgress.swift",
|
||||
"source": "%@ tokens",
|
||||
"surface": "apple",
|
||||
|
||||
@@ -6,13 +6,14 @@ private enum ChatWorkingClawSeed {
|
||||
static let salt = UInt32.random(in: UInt32.min...UInt32.max)
|
||||
}
|
||||
|
||||
private struct ChatWorkingClawPose {
|
||||
struct ChatWorkingClawPose {
|
||||
var bodyRotation: CGFloat = 0
|
||||
var jawRotation: CGFloat = -10
|
||||
var xOffset: CGFloat = 0
|
||||
var yOffset: CGFloat = 0
|
||||
var zRotation: CGFloat = 0
|
||||
var yRotation: CGFloat = 0
|
||||
var bodyScale: CGFloat = 1
|
||||
var powOpacity: Double = 0
|
||||
var powScale: CGFloat = 0.4
|
||||
|
||||
@@ -24,7 +25,24 @@ private struct ChatWorkingClawKeyframe {
|
||||
let value: CGFloat
|
||||
}
|
||||
|
||||
private enum ChatWorkingClawMotion {
|
||||
enum ChatWorkingClawMotion {
|
||||
private enum Easing {
|
||||
case easeOut
|
||||
case easeInOut
|
||||
case linear
|
||||
|
||||
func amount(for linear: Double) -> Double {
|
||||
switch self {
|
||||
case .easeOut:
|
||||
1 - pow(1 - linear, 3)
|
||||
case .easeInOut:
|
||||
UnitCurve.easeInOut.value(at: linear)
|
||||
case .linear:
|
||||
linear
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static let bodySnips = [
|
||||
ChatWorkingClawKeyframe(progress: 0, value: 0),
|
||||
ChatWorkingClawKeyframe(progress: 0.06, value: 0),
|
||||
@@ -99,11 +117,72 @@ private enum ChatWorkingClawMotion {
|
||||
ChatWorkingClawKeyframe(progress: 0.78, value: -360),
|
||||
ChatWorkingClawKeyframe(progress: 1, value: -360),
|
||||
]
|
||||
private static let zenBodyScale = [
|
||||
ChatWorkingClawKeyframe(progress: 0, value: 1),
|
||||
ChatWorkingClawKeyframe(progress: 0.30, value: 1.08),
|
||||
ChatWorkingClawKeyframe(progress: 0.55, value: 1),
|
||||
ChatWorkingClawKeyframe(progress: 1, value: 1),
|
||||
]
|
||||
private static let zenJaw = [
|
||||
ChatWorkingClawKeyframe(progress: 0, value: -10),
|
||||
ChatWorkingClawKeyframe(progress: 0.60, value: -10),
|
||||
ChatWorkingClawKeyframe(progress: 0.70, value: -24),
|
||||
ChatWorkingClawKeyframe(progress: 0.76, value: 2),
|
||||
ChatWorkingClawKeyframe(progress: 0.86, value: -10),
|
||||
ChatWorkingClawKeyframe(progress: 1, value: -10),
|
||||
]
|
||||
private static let drummerBodyRotation = [
|
||||
ChatWorkingClawKeyframe(progress: 0, value: 0),
|
||||
ChatWorkingClawKeyframe(progress: 0.15, value: -8),
|
||||
ChatWorkingClawKeyframe(progress: 0.30, value: 0),
|
||||
ChatWorkingClawKeyframe(progress: 0.55, value: 8),
|
||||
ChatWorkingClawKeyframe(progress: 0.70, value: 0),
|
||||
ChatWorkingClawKeyframe(progress: 1, value: 0),
|
||||
]
|
||||
private static let drummerJaw = [
|
||||
ChatWorkingClawKeyframe(progress: 0, value: -10),
|
||||
ChatWorkingClawKeyframe(progress: 0.10, value: -20),
|
||||
ChatWorkingClawKeyframe(progress: 0.15, value: 2),
|
||||
ChatWorkingClawKeyframe(progress: 0.25, value: -10),
|
||||
ChatWorkingClawKeyframe(progress: 0.50, value: -20),
|
||||
ChatWorkingClawKeyframe(progress: 0.55, value: 2),
|
||||
ChatWorkingClawKeyframe(progress: 0.65, value: -10),
|
||||
ChatWorkingClawKeyframe(progress: 1, value: -10),
|
||||
]
|
||||
private static let peekabooY = [
|
||||
ChatWorkingClawKeyframe(progress: 0, value: 0),
|
||||
ChatWorkingClawKeyframe(progress: 0.55, value: 0),
|
||||
ChatWorkingClawKeyframe(progress: 0.62, value: 5),
|
||||
ChatWorkingClawKeyframe(progress: 0.72, value: 5),
|
||||
ChatWorkingClawKeyframe(progress: 0.78, value: -1.5),
|
||||
ChatWorkingClawKeyframe(progress: 0.84, value: 0),
|
||||
ChatWorkingClawKeyframe(progress: 1, value: 0),
|
||||
]
|
||||
private static let peekabooScale = [
|
||||
ChatWorkingClawKeyframe(progress: 0, value: 1),
|
||||
ChatWorkingClawKeyframe(progress: 0.55, value: 1),
|
||||
ChatWorkingClawKeyframe(progress: 0.62, value: 0.72),
|
||||
ChatWorkingClawKeyframe(progress: 0.72, value: 0.72),
|
||||
ChatWorkingClawKeyframe(progress: 0.78, value: 1.06),
|
||||
ChatWorkingClawKeyframe(progress: 0.84, value: 1),
|
||||
ChatWorkingClawKeyframe(progress: 1, value: 1),
|
||||
]
|
||||
private static let peekabooJaw = [
|
||||
ChatWorkingClawKeyframe(progress: 0, value: -10),
|
||||
ChatWorkingClawKeyframe(progress: 0.55, value: -10),
|
||||
ChatWorkingClawKeyframe(progress: 0.62, value: -2),
|
||||
ChatWorkingClawKeyframe(progress: 0.72, value: -2),
|
||||
ChatWorkingClawKeyframe(progress: 0.78, value: -28),
|
||||
ChatWorkingClawKeyframe(progress: 0.86, value: -10),
|
||||
ChatWorkingClawKeyframe(progress: 1, value: -10),
|
||||
]
|
||||
|
||||
static func pose(stance: ChatWorkingClawStance, elapsed: TimeInterval) -> ChatWorkingClawPose {
|
||||
let duration: TimeInterval = switch stance {
|
||||
case .flurry: 1.3
|
||||
case .spin: 3.6
|
||||
case .zen: 6
|
||||
case .drummer: 1.2
|
||||
default: 2.4
|
||||
}
|
||||
let progress = max(0, elapsed).truncatingRemainder(dividingBy: duration) / duration
|
||||
@@ -128,7 +207,7 @@ private enum ChatWorkingClawMotion {
|
||||
.init(progress: 1, value: 0),
|
||||
],
|
||||
at: progress,
|
||||
eased: false))
|
||||
easing: .linear))
|
||||
pose.powScale = self.sample([
|
||||
.init(progress: 0, value: 0.4),
|
||||
.init(progress: 0.46, value: 0.4),
|
||||
@@ -139,6 +218,16 @@ private enum ChatWorkingClawMotion {
|
||||
case .backflip:
|
||||
pose.yOffset = self.sample(self.backflipY, at: progress)
|
||||
pose.zRotation = self.sample(self.backflipRotation, at: progress)
|
||||
case .zen:
|
||||
pose.bodyScale = self.sample(self.zenBodyScale, at: progress, easing: .easeInOut)
|
||||
pose.jawRotation = self.sample(self.zenJaw, at: progress)
|
||||
case .drummer:
|
||||
pose.bodyRotation = self.sample(self.drummerBodyRotation, at: progress)
|
||||
pose.jawRotation = self.sample(self.drummerJaw, at: progress)
|
||||
case .peekaboo:
|
||||
pose.yOffset = self.sample(self.peekabooY, at: progress)
|
||||
pose.bodyScale = self.sample(self.peekabooScale, at: progress)
|
||||
pose.jawRotation = self.sample(self.peekabooJaw, at: progress)
|
||||
}
|
||||
return pose
|
||||
}
|
||||
@@ -146,14 +235,14 @@ private enum ChatWorkingClawMotion {
|
||||
private static func sample(
|
||||
_ frames: [ChatWorkingClawKeyframe],
|
||||
at progress: Double,
|
||||
eased: Bool = true) -> CGFloat
|
||||
easing: Easing = .easeOut) -> CGFloat
|
||||
{
|
||||
guard let first = frames.first else { return 0 }
|
||||
guard progress > first.progress else { return first.value }
|
||||
for pair in zip(frames, frames.dropFirst()) where progress <= pair.1.progress {
|
||||
let span = pair.1.progress - pair.0.progress
|
||||
let linear = span > 0 ? (progress - pair.0.progress) / span : 1
|
||||
let amount = eased ? 1 - pow(1 - linear, 3) : linear
|
||||
let amount = easing.amount(for: linear)
|
||||
return pair.0.value + (pair.1.value - pair.0.value) * CGFloat(amount)
|
||||
}
|
||||
return frames.last?.value ?? first.value
|
||||
@@ -222,6 +311,7 @@ struct ChatWorkingClawView: View {
|
||||
.degrees(pose.yRotation),
|
||||
axis: (x: 0, y: 1, z: 0),
|
||||
perspective: 0.45)
|
||||
.scaleEffect(pose.bodyScale)
|
||||
.offset(x: pose.xOffset, y: pose.yOffset)
|
||||
.scaleEffect(x: !self.parked && self.stance == .southpaw ? -1 : 1, y: 1)
|
||||
|
||||
|
||||
@@ -7,14 +7,20 @@ enum ChatWorkingClawStance: CaseIterable, Equatable, Sendable {
|
||||
case spin
|
||||
case shadowbox
|
||||
case backflip
|
||||
case zen
|
||||
case drummer
|
||||
case peekaboo
|
||||
|
||||
private static let weightedStances: [(stance: Self, weight: Double)] = [
|
||||
(.standard, 66),
|
||||
(.southpaw, 20),
|
||||
static let weightedStances: [(stance: Self, weight: Double)] = [
|
||||
(.standard, 63),
|
||||
(.southpaw, 19),
|
||||
(.flurry, 5),
|
||||
(.spin, 4),
|
||||
(.shadowbox, 3),
|
||||
(.backflip, 2),
|
||||
(.zen, 2),
|
||||
(.drummer, 1),
|
||||
(.peekaboo, 1),
|
||||
]
|
||||
|
||||
static func seeded(_ key: String, salt: UInt32) -> Self {
|
||||
|
||||
@@ -14,7 +14,93 @@ struct ChatWorkingProgressTests {
|
||||
#expect(first == ChatWorkingClawStance.seeded(run, salt: salt))
|
||||
#expect(first == .southpaw)
|
||||
#expect(ChatWorkingClawStance.seeded("run-a", salt: salt) == .standard)
|
||||
#expect(ChatWorkingClawStance.seeded("run-8", salt: salt) == .spin)
|
||||
#expect(ChatWorkingClawStance.seeded("run-21", salt: salt) == .spin)
|
||||
}
|
||||
|
||||
@Test func `stance weights total one hundred and match the allowlist`() {
|
||||
let weightedStances = ChatWorkingClawStance.weightedStances
|
||||
#expect(weightedStances.reduce(0) { $0 + $1.weight } == 100)
|
||||
#expect(weightedStances.map(\.weight) == [63, 19, 5, 4, 3, 2, 2, 1, 1])
|
||||
#expect(weightedStances.map(\.stance) == [
|
||||
.standard,
|
||||
.southpaw,
|
||||
.flurry,
|
||||
.spin,
|
||||
.shadowbox,
|
||||
.backflip,
|
||||
.zen,
|
||||
.drummer,
|
||||
.peekaboo,
|
||||
])
|
||||
#expect(weightedStances.map(\.stance) == ChatWorkingClawStance.allCases)
|
||||
}
|
||||
|
||||
@Test func `new rare stances are selectable`() {
|
||||
let salt: UInt32 = 0xA1B2_C3D4
|
||||
#expect(ChatWorkingClawStance.seeded("run-10", salt: salt) == .zen)
|
||||
#expect(ChatWorkingClawStance.seeded("run-299", salt: salt) == .drummer)
|
||||
#expect(ChatWorkingClawStance.seeded("run-22", salt: salt) == .peekaboo)
|
||||
}
|
||||
|
||||
@Test func `zen samples the breath and deliberate snip keyframes`() {
|
||||
for (elapsed, scale) in [(0.0, 1.0), (1.8, 1.08), (3.3, 1.0), (6.0, 1.0)] {
|
||||
let pose = ChatWorkingClawMotion.pose(stance: .zen, elapsed: elapsed)
|
||||
self.expectClose(pose.bodyScale, scale)
|
||||
#expect(pose.bodyRotation == 0)
|
||||
#expect(pose.xOffset == 0)
|
||||
#expect(pose.yOffset == 0)
|
||||
}
|
||||
for (elapsed, jaw) in [(3.6, -10.0), (4.2, -24.0), (4.56, 2.0), (5.16, -10.0)] {
|
||||
let pose = ChatWorkingClawMotion.pose(stance: .zen, elapsed: elapsed)
|
||||
self.expectClose(pose.jawRotation, jaw)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `drummer samples the two tilt and jaw hit keyframes`() {
|
||||
for (elapsed, rotation) in [(0.0, 0.0), (0.18, -8.0), (0.36, 0.0), (0.66, 8.0), (0.84, 0.0)] {
|
||||
let pose = ChatWorkingClawMotion.pose(stance: .drummer, elapsed: elapsed)
|
||||
self.expectClose(pose.bodyRotation, rotation)
|
||||
#expect(pose.bodyScale == 1)
|
||||
#expect(pose.xOffset == 0)
|
||||
#expect(pose.yOffset == 0)
|
||||
}
|
||||
for (elapsed, jaw) in [
|
||||
(0.12, -20.0),
|
||||
(0.18, 2.0),
|
||||
(0.30, -10.0),
|
||||
(0.60, -20.0),
|
||||
(0.66, 2.0),
|
||||
(0.78, -10.0),
|
||||
] {
|
||||
let pose = ChatWorkingClawMotion.pose(stance: .drummer, elapsed: elapsed)
|
||||
self.expectClose(pose.jawRotation, jaw)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `peekaboo samples the vertical duck pop and boo keyframes`() {
|
||||
for (elapsed, yOffset, scale) in [
|
||||
(1.32, 0.0, 1.0),
|
||||
(1.488, 5.0, 0.72),
|
||||
(1.728, 5.0, 0.72),
|
||||
(1.872, -1.5, 1.06),
|
||||
(2.016, 0.0, 1.0),
|
||||
] {
|
||||
let pose = ChatWorkingClawMotion.pose(stance: .peekaboo, elapsed: elapsed)
|
||||
self.expectClose(pose.yOffset, yOffset)
|
||||
self.expectClose(pose.bodyScale, scale)
|
||||
#expect(pose.bodyRotation == 0)
|
||||
#expect(pose.xOffset == 0)
|
||||
}
|
||||
for (elapsed, jaw) in [
|
||||
(1.32, -10.0),
|
||||
(1.488, -2.0),
|
||||
(1.728, -2.0),
|
||||
(1.872, -28.0),
|
||||
(2.064, -10.0),
|
||||
] {
|
||||
let pose = ChatWorkingClawMotion.pose(stance: .peekaboo, elapsed: elapsed)
|
||||
self.expectClose(pose.jawRotation, jaw)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `working identity survives run rekey but resets for turn and session`() throws {
|
||||
@@ -349,4 +435,8 @@ struct ChatWorkingProgressTests {
|
||||
runtimeMs: runtimeMs,
|
||||
outputTokens: outputTokens)
|
||||
}
|
||||
|
||||
private func expectClose(_ actual: CGFloat, _ expected: CGFloat) {
|
||||
#expect(abs(actual - expected) < 0.0001)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user