task 04
This commit is contained in:
@@ -1,29 +0,0 @@
|
|||||||
//
|
|
||||||
// CalibrationSampleCollector.swift
|
|
||||||
// Gaze
|
|
||||||
//
|
|
||||||
// Created by Mike Freno on 1/29/26.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct CalibrationSampleCollector {
|
|
||||||
mutating func addSample(
|
|
||||||
to data: inout CalibrationData,
|
|
||||||
step: CalibrationStep,
|
|
||||||
leftRatio: Double?,
|
|
||||||
rightRatio: Double?,
|
|
||||||
leftVertical: Double?,
|
|
||||||
rightVertical: Double?,
|
|
||||||
faceWidthRatio: Double?
|
|
||||||
) {
|
|
||||||
let sample = GazeSample(
|
|
||||||
leftRatio: leftRatio,
|
|
||||||
rightRatio: rightRatio,
|
|
||||||
leftVerticalRatio: leftVertical,
|
|
||||||
rightVerticalRatio: rightVertical,
|
|
||||||
faceWidthRatio: faceWidthRatio
|
|
||||||
)
|
|
||||||
data.addSample(sample, for: step)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
//
|
|
||||||
// CalibrationWindowManager.swift
|
|
||||||
// Gaze
|
|
||||||
//
|
|
||||||
// Manages the fullscreen calibration overlay window.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AppKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
final class CalibrationWindowManager {
|
|
||||||
static let shared = CalibrationWindowManager()
|
|
||||||
|
|
||||||
private var windowController: NSWindowController?
|
|
||||||
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
func showCalibrationOverlay() {
|
|
||||||
guard let screen = NSScreen.main else { return }
|
|
||||||
|
|
||||||
let window = KeyableWindow(
|
|
||||||
contentRect: screen.frame,
|
|
||||||
styleMask: [.borderless, .fullSizeContentView],
|
|
||||||
backing: .buffered,
|
|
||||||
defer: false
|
|
||||||
)
|
|
||||||
|
|
||||||
window.level = .screenSaver
|
|
||||||
window.isOpaque = true
|
|
||||||
window.backgroundColor = .black
|
|
||||||
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
|
||||||
window.acceptsMouseMovedEvents = true
|
|
||||||
window.ignoresMouseEvents = false
|
|
||||||
|
|
||||||
let overlayView = CalibrationOverlayView {
|
|
||||||
self.dismissCalibrationOverlay()
|
|
||||||
}
|
|
||||||
window.contentView = NSHostingView(rootView: overlayView)
|
|
||||||
|
|
||||||
windowController = NSWindowController(window: window)
|
|
||||||
windowController?.showWindow(nil)
|
|
||||||
window.makeKeyAndOrderFront(nil)
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
|
||||||
|
|
||||||
print("🎯 Calibration overlay window opened")
|
|
||||||
}
|
|
||||||
|
|
||||||
func dismissCalibrationOverlay() {
|
|
||||||
windowController?.close()
|
|
||||||
windowController = nil
|
|
||||||
print("🎯 Calibration overlay window closed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,35 @@
|
|||||||
//
|
//
|
||||||
// CalibrationManager.swift
|
// CalibratorService.swift
|
||||||
// Gaze
|
// Gaze
|
||||||
//
|
//
|
||||||
// Created by Mike Freno on 1/15/26.
|
// Created by Mike Freno on 1/29/26.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class CalibrationManager: ObservableObject {
|
final class CalibratorService: ObservableObject {
|
||||||
static let shared = CalibrationManager()
|
static let shared = CalibratorService()
|
||||||
|
|
||||||
// MARK: - Published Properties
|
|
||||||
|
|
||||||
@Published var isCalibrating = false
|
@Published var isCalibrating = false
|
||||||
@Published var isCollectingSamples = false // True when actively collecting (after countdown)
|
@Published var isCollectingSamples = false
|
||||||
@Published var currentStep: CalibrationStep?
|
@Published var currentStep: CalibrationStep?
|
||||||
@Published var currentStepIndex = 0
|
@Published var currentStepIndex = 0
|
||||||
@Published var samplesCollected = 0
|
@Published var samplesCollected = 0
|
||||||
@Published var calibrationData = CalibrationData()
|
@Published var calibrationData = CalibrationData()
|
||||||
|
|
||||||
// MARK: - Configuration
|
private let samplesPerStep = 30
|
||||||
|
|
||||||
private let samplesPerStep = 30 // Collect 30 samples per calibration point (~1 second at 30fps)
|
|
||||||
private let userDefaultsKey = "eyeTrackingCalibration"
|
private let userDefaultsKey = "eyeTrackingCalibration"
|
||||||
|
private let flowController: CalibrationFlowController
|
||||||
|
private var windowController: NSWindowController?
|
||||||
|
|
||||||
private let calibrationSteps: [CalibrationStep] = [
|
private init() {
|
||||||
|
self.flowController = CalibrationFlowController(
|
||||||
|
samplesPerStep: samplesPerStep,
|
||||||
|
calibrationSteps: [
|
||||||
.center,
|
.center,
|
||||||
.left,
|
.left,
|
||||||
.right,
|
.right,
|
||||||
@@ -37,16 +40,6 @@ class CalibrationManager: ObservableObject {
|
|||||||
.topLeft,
|
.topLeft,
|
||||||
.topRight
|
.topRight
|
||||||
]
|
]
|
||||||
|
|
||||||
private let flowController: CalibrationFlowController
|
|
||||||
private var sampleCollector = CalibrationSampleCollector()
|
|
||||||
|
|
||||||
// MARK: - Initialization
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
self.flowController = CalibrationFlowController(
|
|
||||||
samplesPerStep: samplesPerStep,
|
|
||||||
calibrationSteps: calibrationSteps
|
|
||||||
)
|
)
|
||||||
loadCalibration()
|
loadCalibration()
|
||||||
bindFlowController()
|
bindFlowController()
|
||||||
@@ -63,8 +56,6 @@ class CalibrationManager: ObservableObject {
|
|||||||
.assign(to: &$samplesCollected)
|
.assign(to: &$samplesCollected)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Calibration Flow
|
|
||||||
|
|
||||||
func startCalibration() {
|
func startCalibration() {
|
||||||
print("🎯 Starting calibration...")
|
print("🎯 Starting calibration...")
|
||||||
isCalibrating = true
|
isCalibrating = true
|
||||||
@@ -72,7 +63,6 @@ class CalibrationManager: ObservableObject {
|
|||||||
calibrationData = CalibrationData()
|
calibrationData = CalibrationData()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset state for a new calibration attempt (clears isComplete flag from previous calibration)
|
|
||||||
func resetForNewCalibration() {
|
func resetForNewCalibration() {
|
||||||
print("🔄 Resetting for new calibration...")
|
print("🔄 Resetting for new calibration...")
|
||||||
calibrationData = CalibrationData()
|
calibrationData = CalibrationData()
|
||||||
@@ -94,15 +84,14 @@ class CalibrationManager: ObservableObject {
|
|||||||
) {
|
) {
|
||||||
guard isCalibrating, isCollectingSamples, let step = currentStep else { return }
|
guard isCalibrating, isCollectingSamples, let step = currentStep else { return }
|
||||||
|
|
||||||
sampleCollector.addSample(
|
let sample = GazeSample(
|
||||||
to: &calibrationData,
|
|
||||||
step: step,
|
|
||||||
leftRatio: leftRatio,
|
leftRatio: leftRatio,
|
||||||
rightRatio: rightRatio,
|
rightRatio: rightRatio,
|
||||||
leftVertical: leftVertical,
|
leftVerticalRatio: leftVertical,
|
||||||
rightVertical: rightVertical,
|
rightVerticalRatio: rightVertical,
|
||||||
faceWidthRatio: faceWidthRatio
|
faceWidthRatio: faceWidthRatio
|
||||||
)
|
)
|
||||||
|
calibrationData.addSample(sample, for: step)
|
||||||
|
|
||||||
if flowController.markSampleCollected() {
|
if flowController.markSampleCollected() {
|
||||||
advanceToNextStep()
|
advanceToNextStep()
|
||||||
@@ -118,13 +107,48 @@ class CalibrationManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func skipStep() {
|
func skipStep() {
|
||||||
// Allow skipping optional steps (up, down, diagonals)
|
|
||||||
guard isCalibrating, let step = currentStep else { return }
|
guard isCalibrating, let step = currentStep else { return }
|
||||||
|
|
||||||
print("⏭️ Skipping calibration step: \(step.displayName)")
|
print("⏭️ Skipping calibration step: \(step.displayName)")
|
||||||
advanceToNextStep()
|
advanceToNextStep()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showCalibrationOverlay() {
|
||||||
|
guard let screen = NSScreen.main else { return }
|
||||||
|
|
||||||
|
let window = KeyableWindow(
|
||||||
|
contentRect: screen.frame,
|
||||||
|
styleMask: [.borderless, .fullSizeContentView],
|
||||||
|
backing: .buffered,
|
||||||
|
defer: false
|
||||||
|
)
|
||||||
|
|
||||||
|
window.level = .screenSaver
|
||||||
|
window.isOpaque = true
|
||||||
|
window.backgroundColor = .black
|
||||||
|
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
||||||
|
window.acceptsMouseMovedEvents = true
|
||||||
|
window.ignoresMouseEvents = false
|
||||||
|
|
||||||
|
let overlayView = CalibrationOverlayView {
|
||||||
|
self.dismissCalibrationOverlay()
|
||||||
|
}
|
||||||
|
window.contentView = NSHostingView(rootView: overlayView)
|
||||||
|
|
||||||
|
windowController = NSWindowController(window: window)
|
||||||
|
windowController?.showWindow(nil)
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
|
||||||
|
print("🎯 Calibration overlay window opened")
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismissCalibrationOverlay() {
|
||||||
|
windowController?.close()
|
||||||
|
windowController = nil
|
||||||
|
print("🎯 Calibration overlay window closed")
|
||||||
|
}
|
||||||
|
|
||||||
func finishCalibration() {
|
func finishCalibration() {
|
||||||
print("✓ Calibration complete, calculating thresholds...")
|
print("✓ Calibration complete, calculating thresholds...")
|
||||||
|
|
||||||
@@ -150,8 +174,6 @@ class CalibrationManager: ObservableObject {
|
|||||||
CalibrationState.shared.reset()
|
CalibrationState.shared.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Persistence
|
|
||||||
|
|
||||||
private func saveCalibration() {
|
private func saveCalibration() {
|
||||||
do {
|
do {
|
||||||
let encoder = JSONEncoder()
|
let encoder = JSONEncoder()
|
||||||
@@ -189,14 +211,10 @@ class CalibrationManager: ObservableObject {
|
|||||||
func clearCalibration() {
|
func clearCalibration() {
|
||||||
UserDefaults.standard.removeObject(forKey: userDefaultsKey)
|
UserDefaults.standard.removeObject(forKey: userDefaultsKey)
|
||||||
calibrationData = CalibrationData()
|
calibrationData = CalibrationData()
|
||||||
|
|
||||||
CalibrationState.shared.reset()
|
CalibrationState.shared.reset()
|
||||||
|
|
||||||
print("🗑️ Calibration data cleared")
|
print("🗑️ Calibration data cleared")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Validation
|
|
||||||
|
|
||||||
func isCalibrationValid() -> Bool {
|
func isCalibrationValid() -> Bool {
|
||||||
guard calibrationData.isComplete,
|
guard calibrationData.isComplete,
|
||||||
let thresholds = calibrationData.computedThresholds,
|
let thresholds = calibrationData.computedThresholds,
|
||||||
@@ -210,15 +228,12 @@ class CalibrationManager: ObservableObject {
|
|||||||
return !isCalibrationValid()
|
return !isCalibrationValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Apply Calibration
|
|
||||||
|
|
||||||
private func applyCalibration() {
|
private func applyCalibration() {
|
||||||
guard let thresholds = calibrationData.computedThresholds else {
|
guard let thresholds = calibrationData.computedThresholds else {
|
||||||
print("⚠️ No thresholds to apply")
|
print("⚠️ No thresholds to apply")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push to thread-safe state for background processing
|
|
||||||
CalibrationState.shared.setThresholds(thresholds)
|
CalibrationState.shared.setThresholds(thresholds)
|
||||||
CalibrationState.shared.setComplete(true)
|
CalibrationState.shared.setComplete(true)
|
||||||
|
|
||||||
@@ -230,8 +245,6 @@ class CalibrationManager: ObservableObject {
|
|||||||
print(" Screen Bounds: [\(String(format: "%.2f", thresholds.screenRightBound))..\(String(format: "%.2f", thresholds.screenLeftBound))] x [\(String(format: "%.2f", thresholds.screenTopBound))..\(String(format: "%.2f", thresholds.screenBottomBound))]")
|
print(" Screen Bounds: [\(String(format: "%.2f", thresholds.screenRightBound))..\(String(format: "%.2f", thresholds.screenLeftBound))] x [\(String(format: "%.2f", thresholds.screenTopBound))..\(String(format: "%.2f", thresholds.screenBottomBound))]")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Statistics
|
|
||||||
|
|
||||||
func getCalibrationSummary() -> String {
|
func getCalibrationSummary() -> String {
|
||||||
guard calibrationData.isComplete else {
|
guard calibrationData.isComplete else {
|
||||||
return "No calibration data"
|
return "No calibration data"
|
||||||
@@ -252,8 +265,6 @@ class CalibrationManager: ObservableObject {
|
|||||||
return summary
|
return summary
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Progress
|
|
||||||
|
|
||||||
var progress: Double {
|
var progress: Double {
|
||||||
flowController.progress
|
flowController.progress
|
||||||
}
|
}
|
||||||
@@ -261,4 +272,22 @@ class CalibrationManager: ObservableObject {
|
|||||||
var progressText: String {
|
var progressText: String {
|
||||||
flowController.progressText
|
flowController.progressText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func submitSampleToBridge(
|
||||||
|
leftRatio: Double,
|
||||||
|
rightRatio: Double,
|
||||||
|
leftVertical: Double? = nil,
|
||||||
|
rightVertical: Double? = nil,
|
||||||
|
faceWidthRatio: Double = 0
|
||||||
|
) {
|
||||||
|
Task { @MainActor in
|
||||||
|
collectSample(
|
||||||
|
leftRatio: leftRatio,
|
||||||
|
rightRatio: rightRatio,
|
||||||
|
leftVertical: leftVertical,
|
||||||
|
rightVertical: rightVertical,
|
||||||
|
faceWidthRatio: faceWidthRatio
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
//
|
|
||||||
// CalibrationBridge.swift
|
|
||||||
// Gaze
|
|
||||||
//
|
|
||||||
// Thread-safe calibration access for eye tracking.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
final class CalibrationBridge: @unchecked Sendable {
|
|
||||||
nonisolated var thresholds: GazeThresholds? {
|
|
||||||
CalibrationState.shared.thresholds
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated var isComplete: Bool {
|
|
||||||
CalibrationState.shared.isComplete
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated func submitSample(
|
|
||||||
leftRatio: Double,
|
|
||||||
rightRatio: Double,
|
|
||||||
leftVertical: Double?,
|
|
||||||
rightVertical: Double?,
|
|
||||||
faceWidthRatio: Double
|
|
||||||
) {
|
|
||||||
Task { @MainActor in
|
|
||||||
CalibrationManager.shared.collectSample(
|
|
||||||
leftRatio: leftRatio,
|
|
||||||
rightRatio: rightRatio,
|
|
||||||
leftVertical: leftVertical,
|
|
||||||
rightVertical: rightVertical,
|
|
||||||
faceWidthRatio: faceWidthRatio
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,7 +44,6 @@ class EyeTrackingService: NSObject, ObservableObject {
|
|||||||
private let cameraManager = CameraSessionManager()
|
private let cameraManager = CameraSessionManager()
|
||||||
private let visionPipeline = VisionPipeline()
|
private let visionPipeline = VisionPipeline()
|
||||||
private let debugAdapter = EyeDebugStateAdapter()
|
private let debugAdapter = EyeDebugStateAdapter()
|
||||||
private let calibrationBridge = CalibrationBridge()
|
|
||||||
private let gazeDetector: GazeDetector
|
private let gazeDetector: GazeDetector
|
||||||
|
|
||||||
var previewLayer: AVCaptureVideoPreviewLayer? {
|
var previewLayer: AVCaptureVideoPreviewLayer? {
|
||||||
@@ -135,10 +134,11 @@ class EyeTrackingService: NSObject, ObservableObject {
|
|||||||
debugImageSize = debugAdapter.imageSize
|
debugImageSize = debugAdapter.imageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
nonisolated private func updateGazeConfiguration() {
|
@MainActor
|
||||||
|
private func updateGazeConfiguration() {
|
||||||
let configuration = GazeDetector.Configuration(
|
let configuration = GazeDetector.Configuration(
|
||||||
thresholds: calibrationBridge.thresholds,
|
thresholds: CalibrationState.shared.thresholds,
|
||||||
isCalibrationComplete: calibrationBridge.isComplete,
|
isCalibrationComplete: CalibratorService.shared.isCalibrating || CalibrationState.shared.isComplete,
|
||||||
eyeClosedEnabled: EyeTrackingConstants.eyeClosedEnabled,
|
eyeClosedEnabled: EyeTrackingConstants.eyeClosedEnabled,
|
||||||
eyeClosedThreshold: EyeTrackingConstants.eyeClosedThreshold,
|
eyeClosedThreshold: EyeTrackingConstants.eyeClosedThreshold,
|
||||||
yawEnabled: EyeTrackingConstants.yawEnabled,
|
yawEnabled: EyeTrackingConstants.yawEnabled,
|
||||||
@@ -173,8 +173,8 @@ extension EyeTrackingService: CameraSessionDelegate {
|
|||||||
let rightRatio = result.rightPupilRatio,
|
let rightRatio = result.rightPupilRatio,
|
||||||
let faceWidth = result.faceWidthRatio {
|
let faceWidth = result.faceWidthRatio {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
guard CalibrationManager.shared.isCalibrating else { return }
|
guard CalibratorService.shared.isCalibrating else { return }
|
||||||
calibrationBridge.submitSample(
|
CalibratorService.shared.submitSampleToBridge(
|
||||||
leftRatio: leftRatio,
|
leftRatio: leftRatio,
|
||||||
rightRatio: rightRatio,
|
rightRatio: rightRatio,
|
||||||
leftVertical: result.leftVerticalRatio,
|
leftVertical: result.leftVerticalRatio,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Combine
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct CalibrationOverlayView: View {
|
struct CalibrationOverlayView: View {
|
||||||
@StateObject private var calibrationManager = CalibrationManager.shared
|
@StateObject private var calibratorService = CalibratorService.shared
|
||||||
@StateObject private var eyeTrackingService = EyeTrackingService.shared
|
@StateObject private var eyeTrackingService = EyeTrackingService.shared
|
||||||
@StateObject private var viewModel = CalibrationOverlayViewModel()
|
@StateObject private var viewModel = CalibrationOverlayViewModel()
|
||||||
|
|
||||||
@@ -33,10 +33,10 @@ struct CalibrationOverlayView: View {
|
|||||||
errorView(error)
|
errorView(error)
|
||||||
} else if !viewModel.cameraStarted {
|
} else if !viewModel.cameraStarted {
|
||||||
startingCameraView
|
startingCameraView
|
||||||
} else if calibrationManager.isCalibrating {
|
} else if calibratorService.isCalibrating {
|
||||||
calibrationContentView(screenSize: geometry.size)
|
calibrationContentView(screenSize: geometry.size)
|
||||||
} else if viewModel.calibrationStarted
|
} else if viewModel.calibrationStarted
|
||||||
&& calibrationManager.calibrationData.isComplete
|
&& calibratorService.calibrationData.isComplete
|
||||||
{
|
{
|
||||||
// Only show completion if we started calibration this session AND it completed
|
// Only show completion if we started calibration this session AND it completed
|
||||||
completionView
|
completionView
|
||||||
@@ -48,15 +48,15 @@ struct CalibrationOverlayView: View {
|
|||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
await viewModel.startCamera(
|
await viewModel.startCamera(
|
||||||
eyeTrackingService: eyeTrackingService, calibrationManager: calibrationManager)
|
eyeTrackingService: eyeTrackingService, calibratorService: calibratorService)
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
viewModel.cleanup(
|
viewModel.cleanup(
|
||||||
eyeTrackingService: eyeTrackingService, calibrationManager: calibrationManager)
|
eyeTrackingService: eyeTrackingService, calibratorService: calibratorService)
|
||||||
}
|
}
|
||||||
.onChange(of: calibrationManager.currentStep) { oldStep, newStep in
|
.onChange(of: calibratorService.currentStep) { oldStep, newStep in
|
||||||
if newStep != nil && oldStep != newStep {
|
if newStep != nil && oldStep != newStep {
|
||||||
viewModel.startStepCountdown(calibrationManager: calibrationManager)
|
viewModel.startStepCountdown(calibratorService: calibratorService)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ struct CalibrationOverlayView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let step = calibrationManager.currentStep {
|
if let step = calibratorService.currentStep {
|
||||||
calibrationTarget(for: step, screenSize: screenSize)
|
calibrationTarget(for: step, screenSize: screenSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ struct CalibrationOverlayView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
cancelButton
|
cancelButton
|
||||||
Spacer()
|
Spacer()
|
||||||
if !calibrationManager.isCollectingSamples {
|
if !calibratorService.isCollectingSamples {
|
||||||
skipButton
|
skipButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,11 +146,11 @@ struct CalibrationOverlayView: View {
|
|||||||
Text("Calibrating...")
|
Text("Calibrating...")
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(calibrationManager.progressText)
|
Text(calibratorService.progressText)
|
||||||
.foregroundStyle(.white.opacity(0.7))
|
.foregroundStyle(.white.opacity(0.7))
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressView(value: calibrationManager.progress)
|
ProgressView(value: calibratorService.progress)
|
||||||
.progressViewStyle(.linear)
|
.progressViewStyle(.linear)
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
}
|
}
|
||||||
@@ -198,29 +198,29 @@ struct CalibrationOverlayView: View {
|
|||||||
value: viewModel.isCountingDown)
|
value: viewModel.isCountingDown)
|
||||||
|
|
||||||
// Progress ring when collecting
|
// Progress ring when collecting
|
||||||
if calibrationManager.isCollectingSamples {
|
if calibratorService.isCollectingSamples {
|
||||||
Circle()
|
Circle()
|
||||||
.trim(from: 0, to: CGFloat(calibrationManager.samplesCollected) / 30.0)
|
.trim(from: 0, to: CGFloat(calibratorService.samplesCollected) / 30.0)
|
||||||
.stroke(Color.green, lineWidth: 4)
|
.stroke(Color.green, lineWidth: 4)
|
||||||
.frame(width: 90, height: 90)
|
.frame(width: 90, height: 90)
|
||||||
.rotationEffect(.degrees(-90))
|
.rotationEffect(.degrees(-90))
|
||||||
.animation(
|
.animation(
|
||||||
.linear(duration: 0.1), value: calibrationManager.samplesCollected)
|
.linear(duration: 0.1), value: calibratorService.samplesCollected)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inner circle
|
// Inner circle
|
||||||
Circle()
|
Circle()
|
||||||
.fill(calibrationManager.isCollectingSamples ? Color.green : Color.blue)
|
.fill(calibratorService.isCollectingSamples ? Color.green : Color.blue)
|
||||||
.frame(width: 60, height: 60)
|
.frame(width: 60, height: 60)
|
||||||
.animation(
|
.animation(
|
||||||
.easeInOut(duration: 0.3), value: calibrationManager.isCollectingSamples)
|
.easeInOut(duration: 0.3), value: calibratorService.isCollectingSamples)
|
||||||
|
|
||||||
// Countdown number or collecting indicator
|
// Countdown number or collecting indicator
|
||||||
if viewModel.isCountingDown && viewModel.countdownValue > 0 {
|
if viewModel.isCountingDown && viewModel.countdownValue > 0 {
|
||||||
Text("\(viewModel.countdownValue)")
|
Text("\(viewModel.countdownValue)")
|
||||||
.font(.system(size: 36, weight: .bold))
|
.font(.system(size: 36, weight: .bold))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
} else if calibrationManager.isCollectingSamples {
|
} else if calibratorService.isCollectingSamples {
|
||||||
Image(systemName: "eye.fill")
|
Image(systemName: "eye.fill")
|
||||||
.font(.system(size: 24, weight: .bold))
|
.font(.system(size: 24, weight: .bold))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
@@ -241,7 +241,7 @@ struct CalibrationOverlayView: View {
|
|||||||
private func instructionText(for step: CalibrationStep) -> String {
|
private func instructionText(for step: CalibrationStep) -> String {
|
||||||
if viewModel.isCountingDown && viewModel.countdownValue > 0 {
|
if viewModel.isCountingDown && viewModel.countdownValue > 0 {
|
||||||
return "Get ready..."
|
return "Get ready..."
|
||||||
} else if calibrationManager.isCollectingSamples {
|
} else if calibratorService.isCollectingSamples {
|
||||||
return "Look at the target"
|
return "Look at the target"
|
||||||
} else {
|
} else {
|
||||||
return step.instructionText
|
return step.instructionText
|
||||||
@@ -252,7 +252,7 @@ struct CalibrationOverlayView: View {
|
|||||||
|
|
||||||
private var skipButton: some View {
|
private var skipButton: some View {
|
||||||
Button {
|
Button {
|
||||||
viewModel.skipCurrentStep(calibrationManager: calibrationManager)
|
viewModel.skipCurrentStep(calibratorService: calibratorService)
|
||||||
} label: {
|
} label: {
|
||||||
Text("Skip")
|
Text("Skip")
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
@@ -267,7 +267,7 @@ struct CalibrationOverlayView: View {
|
|||||||
private var cancelButton: some View {
|
private var cancelButton: some View {
|
||||||
Button {
|
Button {
|
||||||
viewModel.cleanup(
|
viewModel.cleanup(
|
||||||
eyeTrackingService: eyeTrackingService, calibrationManager: calibrationManager)
|
eyeTrackingService: eyeTrackingService, calibratorService: calibratorService)
|
||||||
onDismiss()
|
onDismiss()
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
@@ -369,7 +369,7 @@ class CalibrationOverlayViewModel: ObservableObject {
|
|||||||
private var lastFaceDetectedTime: Date = .distantPast
|
private var lastFaceDetectedTime: Date = .distantPast
|
||||||
private let faceDetectionDebounce: TimeInterval = 0.5 // 500ms debounce
|
private let faceDetectionDebounce: TimeInterval = 0.5 // 500ms debounce
|
||||||
|
|
||||||
func startCamera(eyeTrackingService: EyeTrackingService, calibrationManager: CalibrationManager)
|
func startCamera(eyeTrackingService: EyeTrackingService, calibratorService: CalibratorService)
|
||||||
async
|
async
|
||||||
{
|
{
|
||||||
do {
|
do {
|
||||||
@@ -382,10 +382,10 @@ class CalibrationOverlayViewModel: ObservableObject {
|
|||||||
try? await Task.sleep(for: .seconds(0.5))
|
try? await Task.sleep(for: .seconds(0.5))
|
||||||
|
|
||||||
// Reset any previous calibration data before starting fresh
|
// Reset any previous calibration data before starting fresh
|
||||||
calibrationManager.resetForNewCalibration()
|
calibratorService.resetForNewCalibration()
|
||||||
calibrationManager.startCalibration()
|
calibratorService.startCalibration()
|
||||||
calibrationStarted = true
|
calibrationStarted = true
|
||||||
startStepCountdown(calibrationManager: calibrationManager)
|
startStepCountdown(calibratorService: calibratorService)
|
||||||
} catch {
|
} catch {
|
||||||
showError = "Failed to start camera: \(error.localizedDescription)"
|
showError = "Failed to start camera: \(error.localizedDescription)"
|
||||||
}
|
}
|
||||||
@@ -411,28 +411,28 @@ class CalibrationOverlayViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanup(eyeTrackingService: EyeTrackingService, calibrationManager: CalibrationManager) {
|
func cleanup(eyeTrackingService: EyeTrackingService, calibratorService: CalibratorService) {
|
||||||
countdownTask?.cancel()
|
countdownTask?.cancel()
|
||||||
countdownTask = nil
|
countdownTask = nil
|
||||||
faceDetectionCancellable?.cancel()
|
faceDetectionCancellable?.cancel()
|
||||||
faceDetectionCancellable = nil
|
faceDetectionCancellable = nil
|
||||||
isCountingDown = false
|
isCountingDown = false
|
||||||
|
|
||||||
if calibrationManager.isCalibrating {
|
if calibratorService.isCalibrating {
|
||||||
calibrationManager.cancelCalibration()
|
calibratorService.cancelCalibration()
|
||||||
}
|
}
|
||||||
|
|
||||||
eyeTrackingService.stopEyeTracking()
|
eyeTrackingService.stopEyeTracking()
|
||||||
}
|
}
|
||||||
|
|
||||||
func skipCurrentStep(calibrationManager: CalibrationManager) {
|
func skipCurrentStep(calibratorService: CalibratorService) {
|
||||||
countdownTask?.cancel()
|
countdownTask?.cancel()
|
||||||
countdownTask = nil
|
countdownTask = nil
|
||||||
isCountingDown = false
|
isCountingDown = false
|
||||||
calibrationManager.skipStep()
|
calibratorService.skipStep()
|
||||||
}
|
}
|
||||||
|
|
||||||
func startStepCountdown(calibrationManager: CalibrationManager) {
|
func startStepCountdown(calibratorService: CalibratorService) {
|
||||||
countdownTask?.cancel()
|
countdownTask?.cancel()
|
||||||
countdownTask = nil
|
countdownTask = nil
|
||||||
countdownValue = 1
|
countdownValue = 1
|
||||||
@@ -446,7 +446,7 @@ class CalibrationOverlayViewModel: ObservableObject {
|
|||||||
// Done counting, start collecting
|
// Done counting, start collecting
|
||||||
isCountingDown = false
|
isCountingDown = false
|
||||||
countdownValue = 0
|
countdownValue = 0
|
||||||
calibrationManager.startCollectingSamples()
|
calibratorService.startCollectingSamples()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct EyeTrackingCalibrationView: View {
|
struct EyeTrackingCalibrationView: View {
|
||||||
@StateObject private var calibrationManager = CalibrationManager.shared
|
@StateObject private var calibratorService = CalibratorService.shared
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -46,12 +46,12 @@ struct EyeTrackingCalibrationView: View {
|
|||||||
}
|
}
|
||||||
.padding(.vertical, 20)
|
.padding(.vertical, 20)
|
||||||
|
|
||||||
if calibrationManager.calibrationData.isComplete {
|
if calibratorService.calibrationData.isComplete {
|
||||||
VStack(spacing: 10) {
|
VStack(spacing: 10) {
|
||||||
Text("Last calibration:")
|
Text("Last calibration:")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.gray)
|
.foregroundStyle(.gray)
|
||||||
Text(calibrationManager.getCalibrationSummary())
|
Text(calibratorService.getCalibrationSummary())
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.foregroundStyle(.gray)
|
.foregroundStyle(.gray)
|
||||||
@@ -86,7 +86,7 @@ struct EyeTrackingCalibrationView: View {
|
|||||||
|
|
||||||
// Small delay to allow sheet dismissal animation
|
// Small delay to allow sheet dismissal animation
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
CalibrationWindowManager.shared.showCalibrationOverlay()
|
CalibratorService.shared.showCalibrationOverlay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ struct EnforceModeSetupView: View {
|
|||||||
@State private var isViewActive = false
|
@State private var isViewActive = false
|
||||||
@State private var showAdvancedSettings = false
|
@State private var showAdvancedSettings = false
|
||||||
@State private var showCalibrationWindow = false
|
@State private var showCalibrationWindow = false
|
||||||
@ObservedObject var calibrationManager = CalibrationManager.shared
|
@ObservedObject var calibratorService = CalibratorService.shared
|
||||||
|
|
||||||
private var cameraHardwareAvailable: Bool {
|
private var cameraHardwareAvailable: Bool {
|
||||||
cameraService.hasCameraHardware
|
cameraService.hasCameraHardware
|
||||||
@@ -155,13 +155,13 @@ struct EnforceModeSetupView: View {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
}
|
}
|
||||||
|
|
||||||
if calibrationManager.calibrationData.isComplete {
|
if calibratorService.calibrationData.isComplete {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text(calibrationManager.getCalibrationSummary())
|
Text(calibratorService.getCalibrationSummary())
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
if calibrationManager.needsRecalibration() {
|
if calibratorService.needsRecalibration() {
|
||||||
Label(
|
Label(
|
||||||
"Calibration expired - recalibration recommended",
|
"Calibration expired - recalibration recommended",
|
||||||
systemImage: "exclamationmark.triangle.fill"
|
systemImage: "exclamationmark.triangle.fill"
|
||||||
@@ -186,7 +186,7 @@ struct EnforceModeSetupView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "target")
|
Image(systemName: "target")
|
||||||
Text(
|
Text(
|
||||||
calibrationManager.calibrationData.isComplete
|
calibratorService.calibrationData.isComplete
|
||||||
? "Recalibrate" : "Run Calibration")
|
? "Recalibrate" : "Run Calibration")
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|||||||
Reference in New Issue
Block a user