@lattices/cli 0.4.14 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/apps/mac/Info.plist +4 -4
- package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/bin/lattices-app.ts +110 -17
- package/bin/lattices-build +125 -0
- package/bin/lattices-dev +89 -16
- package/bin/lattices.ts +977 -16
- package/docs/agents.md +81 -4
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +135 -3
- package/docs/app.md +30 -8
- package/docs/config.md +4 -0
- package/docs/mouse-gestures.md +60 -1
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/reference/dewey.config.ts +2 -2
- package/docs/release.md +171 -0
- package/docs/repo-structure.md +5 -5
- package/docs/voice.md +11 -27
- package/package.json +11 -10
- package/apps/mac/Package.swift +0 -27
- package/apps/mac/Sources/AppShell/App.swift +0 -26
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
- package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
- package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
- package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
- package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
- package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
- package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
- package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
- package/apps/mac/Sources/AppShell/MainView.swift +0 -847
- package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
- package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
- package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
- package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
- package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
- package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
- package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
- package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
- package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
- package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
- package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
- package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
- package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
- package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
- package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
- package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
- package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
- package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
- package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
- package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
- package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
- package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
- package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
- package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
- package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
- package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
- package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
- package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
- package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
- package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
- package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
- package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
- package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
- package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
- package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
- package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
- package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
- package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
- package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
- package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
- package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
- package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
- package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
- package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
- package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
- package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
- package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
- package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
- package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
- package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
- package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
- package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
- package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
- package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
- package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
- package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
- package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
- package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
- package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
- package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
- package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
- package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
- package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
- package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
- package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
- package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
- package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
- package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
- package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
- package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
- package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
- package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
- package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
- package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
- package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
- package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
- package/apps/mac/Sources/Core/System/Capability.swift +0 -79
- package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
- package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
- package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
- package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
- package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
- package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
- package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
- package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
- package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
- package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
- package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
- package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
- package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
- package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
- package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
- package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
- package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
- package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
- package/apps/mac/Sources/UI/ActionRow.swift +0 -78
- package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
- package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
- package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
- package/apps/mac/Sources/UI/Theme.swift +0 -164
- package/apps/mac/Tests/StageDragTests.swift +0 -333
- package/apps/mac/Tests/StageJoinTests.swift +0 -313
- package/apps/mac/Tests/StageManagerTests.swift +0 -280
- package/apps/mac/Tests/StageTileTests.swift +0 -353
- package/swift/Package.swift +0 -20
- package/swift/Sources/DeckKit/DeckAction.swift +0 -51
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
- package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
- package/swift/Sources/DeckKit/DeckHost.swift +0 -7
- package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
- package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
- package/swift/Sources/DeckKit/DeckValue.swift +0 -93
- package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
|
@@ -1,624 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import CoreGraphics
|
|
3
|
-
|
|
4
|
-
// MARK: - Path Point
|
|
5
|
-
|
|
6
|
-
/// A single point in the captured mouse path with timestamp
|
|
7
|
-
struct GesturePathPoint: Codable {
|
|
8
|
-
let x: CGFloat
|
|
9
|
-
let y: CGFloat
|
|
10
|
-
let timestamp: TimeInterval
|
|
11
|
-
|
|
12
|
-
var cgPoint: CGPoint {
|
|
13
|
-
CGPoint(x: x, y: y)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
init(x: CGFloat, y: CGFloat, timestamp: TimeInterval) {
|
|
17
|
-
self.x = x
|
|
18
|
-
self.y = y
|
|
19
|
-
self.timestamp = timestamp
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
init(point: CGPoint, timestamp: TimeInterval) {
|
|
23
|
-
self.x = point.x
|
|
24
|
-
self.y = point.y
|
|
25
|
-
self.timestamp = timestamp
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// MARK: - Direction Segment
|
|
30
|
-
|
|
31
|
-
/// A detected direction segment in the path
|
|
32
|
-
struct DirectionSegment {
|
|
33
|
-
let direction: MouseGestureDirection
|
|
34
|
-
let startIndex: Int
|
|
35
|
-
let endIndex: Int
|
|
36
|
-
let length: CGFloat
|
|
37
|
-
|
|
38
|
-
var label: String {
|
|
39
|
-
direction.rawValue
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// MARK: - Shape Label
|
|
44
|
-
|
|
45
|
-
/// Recognized shape label from the gesture
|
|
46
|
-
enum GestureShapeLabel: String, Codable, CaseIterable {
|
|
47
|
-
// Single segment
|
|
48
|
-
case up = "up"
|
|
49
|
-
case down = "down"
|
|
50
|
-
case left = "left"
|
|
51
|
-
case right = "right"
|
|
52
|
-
|
|
53
|
-
// Two-segment shapes
|
|
54
|
-
case lShapeDownRight = "l-shape-down-right"
|
|
55
|
-
case lShapeDownLeft = "l-shape-down-left"
|
|
56
|
-
case lShapeUpRight = "l-shape-up-right"
|
|
57
|
-
case lShapeUpLeft = "l-shape-up-left"
|
|
58
|
-
case reverseLShapeRightDown = "reverse-l-right-down"
|
|
59
|
-
case reverseLShapeLeftDown = "reverse-l-left-down"
|
|
60
|
-
case vShape = "v-shape"
|
|
61
|
-
case reverseV = "reverse-v"
|
|
62
|
-
case zShape = "z-shape"
|
|
63
|
-
case reverseZ = "reverse-z"
|
|
64
|
-
case sShape = "s-shape"
|
|
65
|
-
|
|
66
|
-
// Three-segment shapes
|
|
67
|
-
case uShape = "u-shape"
|
|
68
|
-
case uShapeFlipped = "u-shape-flipped"
|
|
69
|
-
case nShape = "n-shape"
|
|
70
|
-
case mShape = "m-shape"
|
|
71
|
-
|
|
72
|
-
var displayName: String {
|
|
73
|
-
switch self {
|
|
74
|
-
case .up: return "Up"
|
|
75
|
-
case .down: return "Down"
|
|
76
|
-
case .left: return "Left"
|
|
77
|
-
case .right: return "Right"
|
|
78
|
-
case .lShapeDownRight: return "L (↓ then →)"
|
|
79
|
-
case .lShapeDownLeft: return "L (↓ then ←)"
|
|
80
|
-
case .lShapeUpRight: return "L (↑ then →)"
|
|
81
|
-
case .lShapeUpLeft: return "L (↑ then ←)"
|
|
82
|
-
case .reverseLShapeRightDown: return "Reverse L (→ then ↓)"
|
|
83
|
-
case .reverseLShapeLeftDown: return "Reverse L (← then ↓)"
|
|
84
|
-
case .vShape: return "V (↓ then ↑)"
|
|
85
|
-
case .reverseV: return "Reverse V (↑ then ↓)"
|
|
86
|
-
case .zShape: return "Z (→ then ↓ then →)"
|
|
87
|
-
case .reverseZ: return "Reverse Z (← then ↓ then ←)"
|
|
88
|
-
case .sShape: return "S (→ then ↑ then →)"
|
|
89
|
-
case .uShape: return "U (↓ then → then ↑)"
|
|
90
|
-
case .uShapeFlipped: return "U Flipped (↑ then → then ↓)"
|
|
91
|
-
case .nShape: return "N (↓ then ← then ↑)"
|
|
92
|
-
case .mShape: return "M (↑ then ← then ↓)"
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
var segmentCount: Int {
|
|
97
|
-
switch self {
|
|
98
|
-
case .up, .down, .left, .right:
|
|
99
|
-
return 1
|
|
100
|
-
case .lShapeDownRight, .lShapeDownLeft, .lShapeUpRight, .lShapeUpLeft,
|
|
101
|
-
.reverseLShapeRightDown, .reverseLShapeLeftDown, .vShape, .reverseV:
|
|
102
|
-
return 2
|
|
103
|
-
case .zShape, .reverseZ, .sShape:
|
|
104
|
-
return 3
|
|
105
|
-
case .uShape, .uShapeFlipped, .nShape, .mShape:
|
|
106
|
-
return 3
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
static func from(segments: [DirectionSegment]) -> GestureShapeLabel? {
|
|
111
|
-
guard !segments.isEmpty else { return nil }
|
|
112
|
-
|
|
113
|
-
let directions = segments.map { $0.direction }
|
|
114
|
-
|
|
115
|
-
// Single direction
|
|
116
|
-
if segments.count == 1 {
|
|
117
|
-
switch directions[0] {
|
|
118
|
-
case .up: return .up
|
|
119
|
-
case .down: return .down
|
|
120
|
-
case .left: return .left
|
|
121
|
-
case .right: return .right
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Two directions - L-shapes, V-shapes, etc.
|
|
126
|
-
if segments.count == 2 {
|
|
127
|
-
let first = directions[0]
|
|
128
|
-
let second = directions[1]
|
|
129
|
-
|
|
130
|
-
// L shapes
|
|
131
|
-
if first == .down && second == .right {
|
|
132
|
-
return .lShapeDownRight
|
|
133
|
-
}
|
|
134
|
-
if first == .down && second == .left {
|
|
135
|
-
return .lShapeDownLeft
|
|
136
|
-
}
|
|
137
|
-
if first == .up && second == .right {
|
|
138
|
-
return .lShapeUpRight
|
|
139
|
-
}
|
|
140
|
-
if first == .up && second == .left {
|
|
141
|
-
return .lShapeUpLeft
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Reverse L shapes (horizontal first)
|
|
145
|
-
if first == .right && second == .down {
|
|
146
|
-
return .reverseLShapeRightDown
|
|
147
|
-
}
|
|
148
|
-
if first == .left && second == .down {
|
|
149
|
-
return .reverseLShapeLeftDown
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// V shapes (opposite vertical directions)
|
|
153
|
-
if first == .down && second == .up {
|
|
154
|
-
return .vShape
|
|
155
|
-
}
|
|
156
|
-
if first == .up && second == .down {
|
|
157
|
-
return .reverseV
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Three directions - Z-shapes, U-shapes, etc.
|
|
162
|
-
if segments.count == 3 {
|
|
163
|
-
let first = directions[0]
|
|
164
|
-
let second = directions[1]
|
|
165
|
-
let third = directions[2]
|
|
166
|
-
|
|
167
|
-
// Z shapes (horizontal, vertical, horizontal)
|
|
168
|
-
if first == .right && second == .down && third == .right {
|
|
169
|
-
return .zShape
|
|
170
|
-
}
|
|
171
|
-
if first == .left && second == .down && third == .left {
|
|
172
|
-
return .reverseZ
|
|
173
|
-
}
|
|
174
|
-
if first == .right && second == .up && third == .right {
|
|
175
|
-
return .sShape
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// U shapes (down, right, up or similar)
|
|
179
|
-
if first == .down && second == .right && third == .up {
|
|
180
|
-
return .uShape
|
|
181
|
-
}
|
|
182
|
-
if first == .up && second == .right && third == .down {
|
|
183
|
-
return .uShapeFlipped
|
|
184
|
-
}
|
|
185
|
-
if first == .down && second == .left && third == .up {
|
|
186
|
-
return .nShape
|
|
187
|
-
}
|
|
188
|
-
if first == .up && second == .left && third == .down {
|
|
189
|
-
return .mShape
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return nil
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// MARK: - Recognition Result
|
|
198
|
-
|
|
199
|
-
struct ShapeRecognitionResult {
|
|
200
|
-
let shape: GestureShapeLabel?
|
|
201
|
-
let segments: [DirectionSegment]
|
|
202
|
-
let confidence: CGFloat
|
|
203
|
-
let pathLength: CGFloat
|
|
204
|
-
|
|
205
|
-
var displayLabel: String {
|
|
206
|
-
if let shape {
|
|
207
|
-
return shape.displayName
|
|
208
|
-
}
|
|
209
|
-
if let first = segments.first {
|
|
210
|
-
return "Unknown (\(first.label))"
|
|
211
|
-
}
|
|
212
|
-
return "Unknown"
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
var shapeToken: String? {
|
|
216
|
-
shape?.rawValue
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// MARK: - Shape Recognizer
|
|
221
|
-
|
|
222
|
-
final class ShapeRecognizer {
|
|
223
|
-
// Configuration
|
|
224
|
-
private let minSegmentLength: CGFloat
|
|
225
|
-
private let angularThreshold: CGFloat // radians, default ~45 degrees
|
|
226
|
-
private let minTotalPathLength: CGFloat
|
|
227
|
-
private let cornerSmoothRadius: Int // points to consider around corners
|
|
228
|
-
|
|
229
|
-
init(
|
|
230
|
-
minSegmentLength: CGFloat = 40,
|
|
231
|
-
angularThreshold: CGFloat = .pi / 4, // 45 degrees
|
|
232
|
-
minTotalPathLength: CGFloat = 80,
|
|
233
|
-
cornerSmoothRadius: Int = 3
|
|
234
|
-
) {
|
|
235
|
-
self.minSegmentLength = minSegmentLength
|
|
236
|
-
self.angularThreshold = angularThreshold
|
|
237
|
-
self.minTotalPathLength = minTotalPathLength
|
|
238
|
-
self.cornerSmoothRadius = cornerSmoothRadius
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// MARK: - Main Entry Point
|
|
242
|
-
|
|
243
|
-
func recognize(points: [GesturePathPoint]) -> ShapeRecognitionResult {
|
|
244
|
-
guard points.count >= 3 else {
|
|
245
|
-
return ShapeRecognitionResult(shape: nil, segments: [], confidence: 0, pathLength: 0)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Calculate total path length
|
|
249
|
-
let totalLength = calculatePathLength(points)
|
|
250
|
-
guard totalLength >= minTotalPathLength else {
|
|
251
|
-
return ShapeRecognitionResult(shape: nil, segments: [], confidence: 0, pathLength: totalLength)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Direction runs work better for mouse gestures than strict corner
|
|
255
|
-
// detection because real paths rarely contain one crisp corner sample.
|
|
256
|
-
let runSegments = buildDirectionRunSegments(points: points)
|
|
257
|
-
let corners = detectCorners(points: points)
|
|
258
|
-
let segments = runSegments.isEmpty ? buildSegments(points: points, corners: corners) : runSegments
|
|
259
|
-
|
|
260
|
-
// Classify shape
|
|
261
|
-
let shape = GestureShapeLabel.from(segments: segments)
|
|
262
|
-
|
|
263
|
-
// Calculate confidence based on segment clarity
|
|
264
|
-
let confidence = calculateConfidence(segments: segments, corners: corners.count, totalLength: totalLength)
|
|
265
|
-
|
|
266
|
-
return ShapeRecognitionResult(
|
|
267
|
-
shape: shape,
|
|
268
|
-
segments: segments,
|
|
269
|
-
confidence: confidence,
|
|
270
|
-
pathLength: totalLength
|
|
271
|
-
)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// MARK: - Corner Detection
|
|
275
|
-
|
|
276
|
-
private func detectCorners(points: [GesturePathPoint]) -> [Int] {
|
|
277
|
-
guard points.count > cornerSmoothRadius * 2 else { return [] }
|
|
278
|
-
|
|
279
|
-
var corners: [Int] = []
|
|
280
|
-
var lastSignificantDirection: MouseGestureDirection?
|
|
281
|
-
|
|
282
|
-
for i in cornerSmoothRadius..<(points.count - cornerSmoothRadius) {
|
|
283
|
-
let before = averageVector(in: points, from: i - cornerSmoothRadius, to: i)
|
|
284
|
-
let after = averageVector(in: points, from: i, to: i + cornerSmoothRadius)
|
|
285
|
-
|
|
286
|
-
// Skip if either vector is too short (near-zero movement)
|
|
287
|
-
guard vectorLength(before) > minSegmentLength / 4 else { continue }
|
|
288
|
-
guard vectorLength(after) > minSegmentLength / 4 else { continue }
|
|
289
|
-
|
|
290
|
-
let directionBefore = vectorToDirection(before)
|
|
291
|
-
let directionAfter = vectorToDirection(after)
|
|
292
|
-
|
|
293
|
-
guard let dirBefore = directionBefore, let dirAfter = directionAfter else { continue }
|
|
294
|
-
|
|
295
|
-
// Check if this is a meaningful direction change
|
|
296
|
-
if dirBefore != dirAfter {
|
|
297
|
-
// Verify it's not just noise - the change should be significant
|
|
298
|
-
let angle = angleBetween(before, after)
|
|
299
|
-
if angle >= angularThreshold {
|
|
300
|
-
// Only add if it's a new direction (not rapid back-and-forth)
|
|
301
|
-
if lastSignificantDirection != dirAfter {
|
|
302
|
-
corners.append(i)
|
|
303
|
-
lastSignificantDirection = dirAfter
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return corners
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private func averageVector(in points: [GesturePathPoint], from start: Int, to end: Int) -> CGPoint {
|
|
313
|
-
guard end > start else { return .zero }
|
|
314
|
-
var sum = CGPoint.zero
|
|
315
|
-
var count = 0
|
|
316
|
-
|
|
317
|
-
for i in start..<min(end, points.count) {
|
|
318
|
-
if i > start {
|
|
319
|
-
let prev = points[i - 1]
|
|
320
|
-
let curr = points[i]
|
|
321
|
-
sum.x += curr.x - prev.x
|
|
322
|
-
sum.y += curr.y - prev.y
|
|
323
|
-
count += 1
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return count > 0 ? CGPoint(x: sum.x / CGFloat(count), y: sum.y / CGFloat(count)) : .zero
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
private func vectorLength(_ v: CGPoint) -> CGFloat {
|
|
331
|
-
sqrt(v.x * v.x + v.y * v.y)
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private func vectorToDirection(_ v: CGPoint) -> MouseGestureDirection? {
|
|
335
|
-
let length = vectorLength(v)
|
|
336
|
-
guard length > 5 else { return nil } // minimum threshold
|
|
337
|
-
|
|
338
|
-
// Determine primary direction based on dominant axis
|
|
339
|
-
let absX = abs(v.x)
|
|
340
|
-
let absY = abs(v.y)
|
|
341
|
-
|
|
342
|
-
if absX > absY * 1.2 { // axis bias
|
|
343
|
-
return v.x >= 0 ? .right : .left
|
|
344
|
-
} else if absY > absX * 1.2 {
|
|
345
|
-
return v.y >= 0 ? .down : .up
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// If diagonal, use the dominant component
|
|
349
|
-
if absX >= absY {
|
|
350
|
-
return v.x >= 0 ? .right : .left
|
|
351
|
-
} else {
|
|
352
|
-
return v.y >= 0 ? .down : .up
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private func angleBetween(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
|
|
357
|
-
let dot = a.x * b.x + a.y * b.y
|
|
358
|
-
let lenA = vectorLength(a)
|
|
359
|
-
let lenB = vectorLength(b)
|
|
360
|
-
guard lenA > 0 && lenB > 0 else { return 0 }
|
|
361
|
-
|
|
362
|
-
let cosAngle = max(-1, min(1, dot / (lenA * lenB)))
|
|
363
|
-
return acos(cosAngle)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// MARK: - Segment Building
|
|
367
|
-
|
|
368
|
-
private func makeDirectionSegment(
|
|
369
|
-
direction: MouseGestureDirection?,
|
|
370
|
-
start: Int,
|
|
371
|
-
end: Int,
|
|
372
|
-
length: CGFloat
|
|
373
|
-
) -> DirectionSegment? {
|
|
374
|
-
guard let direction, length > 0 else { return nil }
|
|
375
|
-
return DirectionSegment(
|
|
376
|
-
direction: direction,
|
|
377
|
-
startIndex: start,
|
|
378
|
-
endIndex: end,
|
|
379
|
-
length: length
|
|
380
|
-
)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
private func buildDirectionRunSegments(points: [GesturePathPoint]) -> [DirectionSegment] {
|
|
384
|
-
guard points.count >= 2 else { return [] }
|
|
385
|
-
|
|
386
|
-
var rawSegments: [DirectionSegment] = []
|
|
387
|
-
var currentDirection: MouseGestureDirection?
|
|
388
|
-
var currentStart = 0
|
|
389
|
-
var currentEnd = 0
|
|
390
|
-
var currentLength: CGFloat = 0
|
|
391
|
-
|
|
392
|
-
for index in 1..<points.count {
|
|
393
|
-
let previous = points[index - 1]
|
|
394
|
-
let current = points[index]
|
|
395
|
-
let delta = CGPoint(x: current.x - previous.x, y: current.y - previous.y)
|
|
396
|
-
let length = vectorLength(delta)
|
|
397
|
-
guard length >= 2, let direction = vectorToDirection(delta) else { continue }
|
|
398
|
-
|
|
399
|
-
if currentDirection == nil {
|
|
400
|
-
currentDirection = direction
|
|
401
|
-
currentStart = index - 1
|
|
402
|
-
currentEnd = index
|
|
403
|
-
currentLength = length
|
|
404
|
-
continue
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if direction == currentDirection {
|
|
408
|
-
currentEnd = index
|
|
409
|
-
currentLength += length
|
|
410
|
-
} else {
|
|
411
|
-
if let segment = makeDirectionSegment(
|
|
412
|
-
direction: currentDirection,
|
|
413
|
-
start: currentStart,
|
|
414
|
-
end: currentEnd,
|
|
415
|
-
length: currentLength
|
|
416
|
-
) {
|
|
417
|
-
rawSegments.append(segment)
|
|
418
|
-
}
|
|
419
|
-
currentDirection = direction
|
|
420
|
-
currentStart = index - 1
|
|
421
|
-
currentEnd = index
|
|
422
|
-
currentLength = length
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
if let segment = makeDirectionSegment(
|
|
426
|
-
direction: currentDirection,
|
|
427
|
-
start: currentStart,
|
|
428
|
-
end: currentEnd,
|
|
429
|
-
length: currentLength
|
|
430
|
-
) {
|
|
431
|
-
rawSegments.append(segment)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
let merged = mergeShortDirectionRuns(rawSegments)
|
|
435
|
-
let filtered = merged.filter { $0.length >= minSegmentLength }
|
|
436
|
-
return mergeAdjacentSegments(filtered)
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
private func mergeShortDirectionRuns(_ segments: [DirectionSegment]) -> [DirectionSegment] {
|
|
440
|
-
guard !segments.isEmpty else { return [] }
|
|
441
|
-
|
|
442
|
-
var result: [DirectionSegment] = []
|
|
443
|
-
for segment in segments {
|
|
444
|
-
guard segment.length < minSegmentLength / 2 else {
|
|
445
|
-
result.append(segment)
|
|
446
|
-
continue
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if let last = result.last {
|
|
450
|
-
result[result.count - 1] = DirectionSegment(
|
|
451
|
-
direction: last.direction,
|
|
452
|
-
startIndex: last.startIndex,
|
|
453
|
-
endIndex: segment.endIndex,
|
|
454
|
-
length: last.length + segment.length
|
|
455
|
-
)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return result
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
private func mergeAdjacentSegments(_ segments: [DirectionSegment]) -> [DirectionSegment] {
|
|
463
|
-
guard !segments.isEmpty else { return [] }
|
|
464
|
-
|
|
465
|
-
var result: [DirectionSegment] = []
|
|
466
|
-
for segment in segments {
|
|
467
|
-
if let last = result.last, last.direction == segment.direction {
|
|
468
|
-
result[result.count - 1] = DirectionSegment(
|
|
469
|
-
direction: last.direction,
|
|
470
|
-
startIndex: last.startIndex,
|
|
471
|
-
endIndex: segment.endIndex,
|
|
472
|
-
length: last.length + segment.length
|
|
473
|
-
)
|
|
474
|
-
} else {
|
|
475
|
-
result.append(segment)
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return result
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
private func buildSegments(points: [GesturePathPoint], corners: [Int]) -> [DirectionSegment] {
|
|
483
|
-
guard !points.isEmpty else { return [] }
|
|
484
|
-
|
|
485
|
-
// If no corners, use entire path as one segment
|
|
486
|
-
if corners.isEmpty {
|
|
487
|
-
let direction = overallDirection(points: points)
|
|
488
|
-
if let dir = direction {
|
|
489
|
-
let length = calculatePathLength(points)
|
|
490
|
-
return [DirectionSegment(direction: dir, startIndex: 0, endIndex: points.count - 1, length: length)]
|
|
491
|
-
}
|
|
492
|
-
return []
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
var segments: [DirectionSegment] = []
|
|
496
|
-
|
|
497
|
-
// First segment
|
|
498
|
-
let firstCorner = corners[0]
|
|
499
|
-
let firstDir = segmentDirection(points: points, from: 0, to: firstCorner)
|
|
500
|
-
if let dir = firstDir {
|
|
501
|
-
let length = segmentLength(points: points, from: 0, to: firstCorner)
|
|
502
|
-
if length >= minSegmentLength {
|
|
503
|
-
segments.append(DirectionSegment(direction: dir, startIndex: 0, endIndex: firstCorner, length: length))
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Middle segments (between corners)
|
|
508
|
-
for i in 0..<(corners.count - 1) {
|
|
509
|
-
let startIdx = corners[i]
|
|
510
|
-
let endIdx = corners[i + 1]
|
|
511
|
-
let dir = segmentDirection(points: points, from: startIdx, to: endIdx)
|
|
512
|
-
if let dir = dir {
|
|
513
|
-
let length = segmentLength(points: points, from: startIdx, to: endIdx)
|
|
514
|
-
if length >= minSegmentLength {
|
|
515
|
-
segments.append(DirectionSegment(direction: dir, startIndex: startIdx, endIndex: endIdx, length: length))
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Last segment
|
|
521
|
-
let lastCorner = corners[corners.count - 1]
|
|
522
|
-
let lastDir = segmentDirection(points: points, from: lastCorner, to: points.count - 1)
|
|
523
|
-
if let dir = lastDir {
|
|
524
|
-
let length = segmentLength(points: points, from: lastCorner, to: points.count - 1)
|
|
525
|
-
if length >= minSegmentLength {
|
|
526
|
-
segments.append(DirectionSegment(direction: dir, startIndex: lastCorner, endIndex: points.count - 1, length: length))
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return segments
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
private func overallDirection(points: [GesturePathPoint]) -> MouseGestureDirection? {
|
|
534
|
-
guard let first = points.first, let last = points.last else { return nil }
|
|
535
|
-
let delta = CGPoint(x: last.x - first.x, y: last.y - first.y)
|
|
536
|
-
return vectorToDirection(delta)
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
private func segmentDirection(points: [GesturePathPoint], from startIdx: Int, to endIdx: Int) -> MouseGestureDirection? {
|
|
540
|
-
guard startIdx < endIdx, endIdx < points.count else { return nil }
|
|
541
|
-
let start = points[startIdx]
|
|
542
|
-
let end = points[endIdx]
|
|
543
|
-
let delta = CGPoint(x: end.x - start.x, y: end.y - start.y)
|
|
544
|
-
return vectorToDirection(delta)
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
private func segmentLength(points: [GesturePathPoint], from startIdx: Int, to endIdx: Int) -> CGFloat {
|
|
548
|
-
guard startIdx < endIdx else { return 0 }
|
|
549
|
-
var length: CGFloat = 0
|
|
550
|
-
for i in (startIdx + 1)...endIdx {
|
|
551
|
-
if i < points.count {
|
|
552
|
-
let dx = points[i].x - points[i - 1].x
|
|
553
|
-
let dy = points[i].y - points[i - 1].y
|
|
554
|
-
length += sqrt(dx * dx + dy * dy)
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return length
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
private func calculatePathLength(_ points: [GesturePathPoint]) -> CGFloat {
|
|
561
|
-
guard points.count >= 2 else { return 0 }
|
|
562
|
-
var length: CGFloat = 0
|
|
563
|
-
for i in 1..<points.count {
|
|
564
|
-
let dx = points[i].x - points[i - 1].x
|
|
565
|
-
let dy = points[i].y - points[i - 1].y
|
|
566
|
-
length += sqrt(dx * dx + dy * dy)
|
|
567
|
-
}
|
|
568
|
-
return length
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// MARK: - Confidence Calculation
|
|
572
|
-
|
|
573
|
-
private func calculateConfidence(segments: [DirectionSegment], corners: Int, totalLength: CGFloat) -> CGFloat {
|
|
574
|
-
guard !segments.isEmpty else { return 0 }
|
|
575
|
-
|
|
576
|
-
var confidence: CGFloat = 1.0
|
|
577
|
-
|
|
578
|
-
// Penalize for many corners (noisy path)
|
|
579
|
-
if corners > 2 {
|
|
580
|
-
confidence -= CGFloat(corners - 2) * 0.1
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Penalize if segment lengths are very uneven (might be accidental)
|
|
584
|
-
if segments.count >= 2 {
|
|
585
|
-
let lengths = segments.map { $0.length }
|
|
586
|
-
let avgLength = lengths.reduce(0, +) / CGFloat(lengths.count)
|
|
587
|
-
let variance = lengths.map { abs($0 - avgLength) / avgLength }.reduce(0, +) / CGFloat(lengths.count)
|
|
588
|
-
confidence -= min(0.3, CGFloat(variance) * 0.5)
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Boost if path is long and smooth
|
|
592
|
-
if totalLength > 200 && corners == 0 {
|
|
593
|
-
confidence = min(1.0, confidence + 0.1)
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
return max(0, min(1, confidence))
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// MARK: - Convenience Extensions
|
|
601
|
-
|
|
602
|
-
extension ShapeRecognizer {
|
|
603
|
-
/// Recognize from raw CGPoints
|
|
604
|
-
func recognize(points: [CGPoint], timestamps: [TimeInterval]? = nil) -> ShapeRecognitionResult {
|
|
605
|
-
let gesturePoints: [GesturePathPoint]
|
|
606
|
-
if let ts = timestamps {
|
|
607
|
-
gesturePoints = zip(points, ts).map { GesturePathPoint(x: $0.0.x, y: $0.0.y, timestamp: $0.1) }
|
|
608
|
-
} else {
|
|
609
|
-
let now = Date().timeIntervalSinceReferenceDate
|
|
610
|
-
gesturePoints = points.enumerated().map { GesturePathPoint(x: $0.1.x, y: $0.1.y, timestamp: now + Double($0.0) * 0.01) }
|
|
611
|
-
}
|
|
612
|
-
return recognize(points: gesturePoints)
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/// Quick check if path contains a corner
|
|
616
|
-
func hasCorner(at point: CGPoint, in points: [GesturePathPoint]) -> Bool {
|
|
617
|
-
// Simple check - find nearest point index and check context
|
|
618
|
-
guard let nearestIndex = points.firstIndex(where: { abs($0.x - point.x) < 5 && abs($0.y - point.y) < 5 }) else {
|
|
619
|
-
return false
|
|
620
|
-
}
|
|
621
|
-
let corners = detectCorners(points: points)
|
|
622
|
-
return corners.contains(nearestIndex)
|
|
623
|
-
}
|
|
624
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
/// Measures CGEventTap callback duration and logs throttled warnings when
|
|
4
|
-
/// callbacks exceed the budget. Thread-safe — designed to be invoked from
|
|
5
|
-
/// the event-tap thread on every event.
|
|
6
|
-
///
|
|
7
|
-
/// Logging is throttled to at most one warning per second per meter, with
|
|
8
|
-
/// the peak value observed in that window. This keeps the log readable
|
|
9
|
-
/// when something is misbehaving without losing the signal.
|
|
10
|
-
///
|
|
11
|
-
/// Why this exists: the whole point of the EventTapThread off-main move is
|
|
12
|
-
/// "tap callbacks are now fast." This is the measurement that confirms it
|
|
13
|
-
/// stays true and surfaces regressions before they cause `tapDisabledByTimeout`.
|
|
14
|
-
final class TapBudgetMeter {
|
|
15
|
-
private let label: String
|
|
16
|
-
private let warnThresholdMs: Double
|
|
17
|
-
private let throttleSec: TimeInterval
|
|
18
|
-
|
|
19
|
-
private let lock = NSLock()
|
|
20
|
-
private var maxMsInWindow: Double = 0
|
|
21
|
-
private var samplesInWindow: Int = 0
|
|
22
|
-
private var lastLog: Date = .distantPast
|
|
23
|
-
|
|
24
|
-
init(label: String, warnThresholdMs: Double = 5.0, throttleSec: TimeInterval = 1.0) {
|
|
25
|
-
self.label = label
|
|
26
|
-
self.warnThresholdMs = warnThresholdMs
|
|
27
|
-
self.throttleSec = throttleSec
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/// Records one callback's wall-clock duration. No-op when below the
|
|
31
|
-
/// threshold. Logs at most once per `throttleSec` window.
|
|
32
|
-
func record(durationMs: Double) {
|
|
33
|
-
guard durationMs > warnThresholdMs else { return }
|
|
34
|
-
|
|
35
|
-
lock.lock()
|
|
36
|
-
if durationMs > maxMsInWindow { maxMsInWindow = durationMs }
|
|
37
|
-
samplesInWindow += 1
|
|
38
|
-
|
|
39
|
-
let now = Date()
|
|
40
|
-
guard now.timeIntervalSince(lastLog) > throttleSec else {
|
|
41
|
-
lock.unlock()
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let peak = maxMsInWindow
|
|
46
|
-
let count = samplesInWindow
|
|
47
|
-
maxMsInWindow = 0
|
|
48
|
-
samplesInWindow = 0
|
|
49
|
-
lastLog = now
|
|
50
|
-
lock.unlock()
|
|
51
|
-
|
|
52
|
-
DiagnosticLog.shared.warn(
|
|
53
|
-
"\(label): tap callback peak \(Int(peak))ms (× \(count) over threshold \(Int(warnThresholdMs))ms in last \(Int(throttleSec))s)"
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
}
|