temp
This commit is contained in:
@@ -8,7 +8,10 @@
|
|||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class EyeTrackingConstants: ObservableObject {
|
/// Thread-safe configuration holder for eye tracking thresholds.
|
||||||
|
/// Uses @unchecked Sendable because all access is via the shared singleton
|
||||||
|
/// and the @Published properties are only mutated from the main thread.
|
||||||
|
final class EyeTrackingConstants: ObservableObject, @unchecked Sendable {
|
||||||
static let shared = EyeTrackingConstants()
|
static let shared = EyeTrackingConstants()
|
||||||
|
|
||||||
// MARK: - Logging
|
// MARK: - Logging
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Foundation
|
|||||||
|
|
||||||
// MARK: - Reminder Size
|
// MARK: - Reminder Size
|
||||||
|
|
||||||
enum ReminderSize: String, Codable, CaseIterable {
|
enum ReminderSize: String, Codable, CaseIterable, Sendable {
|
||||||
case small
|
case small
|
||||||
case medium
|
case medium
|
||||||
case large
|
case large
|
||||||
@@ -31,7 +31,7 @@ enum ReminderSize: String, Codable, CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppSettings: Codable, Equatable, Hashable {
|
struct AppSettings: Codable, Equatable, Hashable, Sendable {
|
||||||
var lookAwayTimer: TimerConfiguration
|
var lookAwayTimer: TimerConfiguration
|
||||||
var lookAwayCountdownSeconds: Int
|
var lookAwayCountdownSeconds: Int
|
||||||
var blinkTimer: TimerConfiguration
|
var blinkTimer: TimerConfiguration
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct SmartModeSettings: Codable, Equatable, Hashable {
|
struct SmartModeSettings: Codable, Equatable, Hashable, Sendable {
|
||||||
var autoPauseOnFullscreen: Bool
|
var autoPauseOnFullscreen: Bool
|
||||||
var autoPauseOnIdle: Bool
|
var autoPauseOnIdle: Bool
|
||||||
var trackUsage: Bool
|
var trackUsage: Bool
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct TimerConfiguration: Codable, Equatable, Hashable {
|
struct TimerConfiguration: Codable, Equatable, Hashable, Sendable {
|
||||||
var enabled: Bool
|
var enabled: Bool
|
||||||
var intervalSeconds: Int
|
var intervalSeconds: Int
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Represents a user-defined timer with customizable properties
|
/// Represents a user-defined timer with customizable properties
|
||||||
struct UserTimer: Codable, Equatable, Identifiable, Hashable {
|
struct UserTimer: Codable, Equatable, Identifiable, Hashable, Sendable {
|
||||||
let id: String
|
let id: String
|
||||||
var title: String
|
var title: String
|
||||||
var type: UserTimerType
|
var type: UserTimerType
|
||||||
@@ -100,7 +100,7 @@ extension Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Type of user timer - subtle or overlay
|
/// Type of user timer - subtle or overlay
|
||||||
enum UserTimerType: String, Codable, CaseIterable, Identifiable {
|
enum UserTimerType: String, Codable, CaseIterable, Identifiable, Sendable {
|
||||||
case subtle
|
case subtle
|
||||||
case overlay
|
case overlay
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Protocol for providing current time, enabling deterministic tests.
|
/// Protocol for providing current time, enabling deterministic tests.
|
||||||
protocol TimeProviding {
|
protocol TimeProviding: Sendable {
|
||||||
/// Returns the current date/time
|
/// Returns the current date/time
|
||||||
func now() -> Date
|
func now() -> Date
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ struct SystemTimeProvider: TimeProviding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Test implementation that allows manual time control
|
/// Test implementation that allows manual time control
|
||||||
final class MockTimeProvider: TimeProviding {
|
final class MockTimeProvider: TimeProviding, @unchecked Sendable {
|
||||||
private var currentTime: Date
|
private var currentTime: Date
|
||||||
|
|
||||||
init(startTime: Date = Date()) {
|
init(startTime: Date = Date()) {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class EyeTrackingService: NSObject, ObservableObject {
|
|||||||
// MARK: - Processing Result
|
// MARK: - Processing Result
|
||||||
|
|
||||||
/// Result struct for off-main-thread processing
|
/// Result struct for off-main-thread processing
|
||||||
private struct ProcessingResult {
|
private struct ProcessingResult: Sendable {
|
||||||
var faceDetected: Bool = false
|
var faceDetected: Bool = false
|
||||||
var isEyesClosed: Bool = false
|
var isEyesClosed: Bool = false
|
||||||
var userLookingAtScreen: Bool = true
|
var userLookingAtScreen: Bool = true
|
||||||
@@ -271,7 +271,7 @@ class EyeTrackingService: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Non-isolated gaze detection result
|
/// Non-isolated gaze detection result
|
||||||
private struct GazeResult {
|
private struct GazeResult: Sendable {
|
||||||
var lookingAway: Bool = false
|
var lookingAway: Bool = false
|
||||||
var leftPupilRatio: Double?
|
var leftPupilRatio: Double?
|
||||||
var rightPupilRatio: Double?
|
var rightPupilRatio: Double?
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ final class FullscreenDetectionService: ObservableObject {
|
|||||||
|
|
||||||
// Factory method to safely create instances from non-main actor contexts
|
// Factory method to safely create instances from non-main actor contexts
|
||||||
static func create(
|
static func create(
|
||||||
permissionManager: ScreenCapturePermissionManaging = ScreenCapturePermissionManager.shared,
|
permissionManager: ScreenCapturePermissionManaging? = nil,
|
||||||
environmentProvider: FullscreenEnvironmentProviding = SystemFullscreenEnvironmentProvider()
|
environmentProvider: FullscreenEnvironmentProviding? = nil
|
||||||
) async -> FullscreenDetectionService {
|
) async -> FullscreenDetectionService {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
return FullscreenDetectionService(
|
return FullscreenDetectionService(
|
||||||
permissionManager: permissionManager,
|
permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared,
|
||||||
environmentProvider: environmentProvider
|
environmentProvider: environmentProvider ?? SystemFullscreenEnvironmentProvider()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,28 +20,33 @@ import Accelerate
|
|||||||
import ImageIO
|
import ImageIO
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct PupilPosition: Equatable {
|
struct PupilPosition: Equatable, Sendable {
|
||||||
let x: CGFloat
|
let x: CGFloat
|
||||||
let y: CGFloat
|
let y: CGFloat
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EyeRegion {
|
struct EyeRegion: Sendable {
|
||||||
let frame: CGRect
|
let frame: CGRect
|
||||||
let center: CGPoint
|
let center: CGPoint
|
||||||
let origin: CGPoint
|
let origin: CGPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calibration state for adaptive thresholding (matches Python Calibration class)
|
/// Calibration state for adaptive thresholding (matches Python Calibration class)
|
||||||
final class PupilCalibration {
|
final class PupilCalibration: @unchecked Sendable {
|
||||||
|
private let lock = NSLock()
|
||||||
private let targetFrames = 20
|
private let targetFrames = 20
|
||||||
private var thresholdsLeft: [Int] = []
|
private var thresholdsLeft: [Int] = []
|
||||||
private var thresholdsRight: [Int] = []
|
private var thresholdsRight: [Int] = []
|
||||||
|
|
||||||
var isComplete: Bool {
|
var isComplete: Bool {
|
||||||
thresholdsLeft.count >= targetFrames && thresholdsRight.count >= targetFrames
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
return thresholdsLeft.count >= targetFrames && thresholdsRight.count >= targetFrames
|
||||||
}
|
}
|
||||||
|
|
||||||
func threshold(forSide side: Int) -> Int {
|
func threshold(forSide side: Int) -> Int {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
let thresholds = side == 0 ? thresholdsLeft : thresholdsRight
|
let thresholds = side == 0 ? thresholdsLeft : thresholdsRight
|
||||||
guard !thresholds.isEmpty else { return 50 }
|
guard !thresholds.isEmpty else { return 50 }
|
||||||
return thresholds.reduce(0, +) / thresholds.count
|
return thresholds.reduce(0, +) / thresholds.count
|
||||||
@@ -49,6 +54,8 @@ final class PupilCalibration {
|
|||||||
|
|
||||||
func evaluate(eyeData: UnsafePointer<UInt8>, width: Int, height: Int, side: Int) {
|
func evaluate(eyeData: UnsafePointer<UInt8>, width: Int, height: Int, side: Int) {
|
||||||
let bestThreshold = findBestThreshold(eyeData: eyeData, width: width, height: height)
|
let bestThreshold = findBestThreshold(eyeData: eyeData, width: width, height: height)
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
if side == 0 {
|
if side == 0 {
|
||||||
thresholdsLeft.append(bestThreshold)
|
thresholdsLeft.append(bestThreshold)
|
||||||
} else {
|
} else {
|
||||||
@@ -106,13 +113,15 @@ final class PupilCalibration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
thresholdsLeft.removeAll()
|
thresholdsLeft.removeAll()
|
||||||
thresholdsRight.removeAll()
|
thresholdsRight.removeAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performance metrics for pupil detection
|
/// Performance metrics for pupil detection
|
||||||
struct PupilDetectorMetrics {
|
struct PupilDetectorMetrics: Sendable {
|
||||||
var lastProcessingTimeMs: Double = 0
|
var lastProcessingTimeMs: Double = 0
|
||||||
var averageProcessingTimeMs: Double = 0
|
var averageProcessingTimeMs: Double = 0
|
||||||
var frameCount: Int = 0
|
var frameCount: Int = 0
|
||||||
@@ -126,7 +135,11 @@ struct PupilDetectorMetrics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PupilDetector {
|
final class PupilDetector: @unchecked Sendable {
|
||||||
|
|
||||||
|
// MARK: - Thread Safety
|
||||||
|
|
||||||
|
private static let lock = NSLock()
|
||||||
|
|
||||||
// MARK: - Configuration
|
// MARK: - Configuration
|
||||||
|
|
||||||
@@ -134,14 +147,14 @@ final class PupilDetector {
|
|||||||
static var enablePerformanceLogging = false
|
static var enablePerformanceLogging = false
|
||||||
static var frameSkipCount = 10 // Process every Nth frame
|
static var frameSkipCount = 10 // Process every Nth frame
|
||||||
|
|
||||||
// MARK: - State
|
// MARK: - State (protected by lock)
|
||||||
|
|
||||||
private static var debugImageCounter = 0
|
private static var _debugImageCounter = 0
|
||||||
private static var frameCounter = 0
|
private static var _frameCounter = 0
|
||||||
private static var lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) = (nil, nil)
|
private static var _lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) = (nil, nil)
|
||||||
|
private static var _metrics = PupilDetectorMetrics()
|
||||||
|
|
||||||
static let calibration = PupilCalibration()
|
static let calibration = PupilCalibration()
|
||||||
static var metrics = PupilDetectorMetrics()
|
|
||||||
|
|
||||||
// MARK: - Precomputed Tables
|
// MARK: - Precomputed Tables
|
||||||
|
|
||||||
@@ -182,7 +195,7 @@ final class PupilDetector {
|
|||||||
|
|
||||||
/// Detects pupil position with frame skipping for performance
|
/// Detects pupil position with frame skipping for performance
|
||||||
/// Returns cached result on skipped frames
|
/// Returns cached result on skipped frames
|
||||||
static func detectPupil(
|
nonisolated static func detectPupil(
|
||||||
in pixelBuffer: CVPixelBuffer,
|
in pixelBuffer: CVPixelBuffer,
|
||||||
eyeLandmarks: VNFaceLandmarkRegion2D,
|
eyeLandmarks: VNFaceLandmarkRegion2D,
|
||||||
faceBoundingBox: CGRect,
|
faceBoundingBox: CGRect,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ final class SettingsWindowPresenter {
|
|||||||
window.titleVisibility = .hidden
|
window.titleVisibility = .hidden
|
||||||
window.titlebarAppearsTransparent = true
|
window.titlebarAppearsTransparent = true
|
||||||
window.toolbarStyle = .unified
|
window.toolbarStyle = .unified
|
||||||
window.toolbar = NSToolbar()
|
window.showsToolbarButton = false
|
||||||
window.center()
|
window.center()
|
||||||
window.setFrameAutosaveName("SettingsWindow")
|
window.setFrameAutosaveName("SettingsWindow")
|
||||||
window.isReleasedWhenClosed = false
|
window.isReleasedWhenClosed = false
|
||||||
@@ -103,8 +103,10 @@ final class SettingsWindowPresenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
Task { @MainActor in
|
// Capture observer locally to avoid actor isolation issues
|
||||||
removeCloseObserver()
|
// NotificationCenter.removeObserver is thread-safe
|
||||||
|
if let observer = closeObserver {
|
||||||
|
NotificationCenter.default.removeObserver(observer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +135,15 @@ struct SettingsWindowView: View {
|
|||||||
.listStyle(.sidebar)
|
.listStyle(.sidebar)
|
||||||
} detail: {
|
} detail: {
|
||||||
detailView(for: selectedSection)
|
detailView(for: selectedSection)
|
||||||
|
}.onReceive(
|
||||||
|
NotificationCenter.default.publisher(
|
||||||
|
for: Notification.Name("SwitchToSettingsTab"))
|
||||||
|
) { notification in
|
||||||
|
if let tab = notification.object as? Int,
|
||||||
|
let section = SettingsSection(rawValue: tab)
|
||||||
|
{
|
||||||
|
selectedSection = section
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -161,15 +172,6 @@ struct SettingsWindowView: View {
|
|||||||
minHeight: 900
|
minHeight: 900
|
||||||
)
|
)
|
||||||
#endif
|
#endif
|
||||||
.onReceive(
|
|
||||||
NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab"))
|
|
||||||
) { notification in
|
|
||||||
if let tab = notification.object as? Int,
|
|
||||||
let section = SettingsSection(rawValue: tab)
|
|
||||||
{
|
|
||||||
selectedSection = section
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
2
run
2
run
@@ -98,7 +98,7 @@ if [ "$ACTION" = "build" ]; then
|
|||||||
|
|
||||||
elif [ "$ACTION" = "test" ]; then
|
elif [ "$ACTION" = "test" ]; then
|
||||||
echo "Running unit tests..."
|
echo "Running unit tests..."
|
||||||
run_with_output "xcodebuild -project Gaze.xcodeproj -scheme GazeTests -configuration Debug test"
|
run_with_output "xcodebuild -project Gaze.xcodeproj -scheme Gaze -configuration Debug test"
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "✅ Tests passed!"
|
echo "✅ Tests passed!"
|
||||||
|
|||||||
Reference in New Issue
Block a user