tweaking
This commit is contained in:
@@ -42,6 +42,7 @@ struct AppSettings: Codable, Equatable, Hashable, Sendable {
|
||||
var subtleReminderSize: ReminderSize
|
||||
|
||||
var smartMode: SmartModeSettings
|
||||
var enforceModeStrictness: Double
|
||||
|
||||
var hasCompletedOnboarding: Bool
|
||||
var launchAtLogin: Bool
|
||||
@@ -57,6 +58,7 @@ struct AppSettings: Codable, Equatable, Hashable, Sendable {
|
||||
userTimers: [UserTimer] = [],
|
||||
subtleReminderSize: ReminderSize = DefaultSettingsBuilder.subtleReminderSize,
|
||||
smartMode: SmartModeSettings = DefaultSettingsBuilder.smartMode,
|
||||
enforceModeStrictness: Double = DefaultSettingsBuilder.enforceModeStrictness,
|
||||
hasCompletedOnboarding: Bool = DefaultSettingsBuilder.hasCompletedOnboarding,
|
||||
launchAtLogin: Bool = DefaultSettingsBuilder.launchAtLogin,
|
||||
playSounds: Bool = DefaultSettingsBuilder.playSounds
|
||||
@@ -70,6 +72,7 @@ struct AppSettings: Codable, Equatable, Hashable, Sendable {
|
||||
self.userTimers = userTimers
|
||||
self.subtleReminderSize = subtleReminderSize
|
||||
self.smartMode = smartMode
|
||||
self.enforceModeStrictness = enforceModeStrictness
|
||||
self.hasCompletedOnboarding = hasCompletedOnboarding
|
||||
self.launchAtLogin = launchAtLogin
|
||||
self.playSounds = playSounds
|
||||
|
||||
@@ -16,6 +16,7 @@ struct DefaultSettingsBuilder {
|
||||
static let postureIntervalMinutes = 30
|
||||
static let subtleReminderSize: ReminderSize = .medium
|
||||
static let smartMode: SmartModeSettings = .defaults
|
||||
static let enforceModeStrictness = 0.4
|
||||
static let hasCompletedOnboarding = false
|
||||
static let launchAtLogin = false
|
||||
static let playSounds = true
|
||||
@@ -31,6 +32,7 @@ struct DefaultSettingsBuilder {
|
||||
userTimers: [],
|
||||
subtleReminderSize: subtleReminderSize,
|
||||
smartMode: smartMode,
|
||||
enforceModeStrictness: enforceModeStrictness,
|
||||
hasCompletedOnboarding: hasCompletedOnboarding,
|
||||
launchAtLogin: launchAtLogin,
|
||||
playSounds: playSounds
|
||||
|
||||
@@ -31,6 +31,8 @@ class EnforceModeService: ObservableObject {
|
||||
private var timerEngine: TimerEngine?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var faceDetectionTimer: Timer?
|
||||
private var trackingDebugTimer: Timer?
|
||||
private var trackingLapStats = TrackingLapStats()
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
@@ -83,6 +85,17 @@ class EnforceModeService: ObservableObject {
|
||||
self?.refreshEnforceModeState()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
$isCameraActive
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] isActive in
|
||||
if isActive {
|
||||
self?.startTrackingDebugTimer()
|
||||
} else {
|
||||
self?.stopTrackingDebugTimer()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func refreshEnforceModeState() {
|
||||
@@ -211,6 +224,7 @@ class EnforceModeService: ObservableObject {
|
||||
eyeTrackingService.stopEyeTracking()
|
||||
isCameraActive = false
|
||||
stopFaceDetectionTimer()
|
||||
stopTrackingDebugTimer()
|
||||
userCompliedWithBreak = false
|
||||
}
|
||||
|
||||
@@ -257,6 +271,48 @@ class EnforceModeService: ObservableObject {
|
||||
faceDetectionTimer = nil
|
||||
}
|
||||
|
||||
private func startTrackingDebugTimer() {
|
||||
stopTrackingDebugTimer()
|
||||
trackingDebugTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] _ in
|
||||
self?.logTrackingDebugSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopTrackingDebugTimer() {
|
||||
trackingDebugTimer?.invalidate()
|
||||
trackingDebugTimer = nil
|
||||
}
|
||||
|
||||
private func logTrackingDebugSnapshot() {
|
||||
guard isCameraActive else { return }
|
||||
|
||||
let debugState = eyeTrackingService.debugState
|
||||
let faceWidth = debugState.faceWidthRatio.map { String(format: "%.3f", $0) } ?? "-"
|
||||
let horizontal = debugState.normalizedHorizontal.map { String(format: "%.3f", $0) } ?? "-"
|
||||
let vertical = debugState.normalizedVertical.map { String(format: "%.3f", $0) } ?? "-"
|
||||
|
||||
trackingLapStats.ingest(
|
||||
faceWidth: debugState.faceWidthRatio,
|
||||
horizontal: debugState.normalizedHorizontal,
|
||||
vertical: debugState.normalizedVertical
|
||||
)
|
||||
|
||||
logDebug(
|
||||
"📊 Tracking | faceWidth=\(faceWidth) | h=\(horizontal) | v=\(vertical)",
|
||||
category: "EyeTracking"
|
||||
)
|
||||
}
|
||||
|
||||
func logTrackingLap() {
|
||||
logDebug("🏁 Tracking Lap", category: "EyeTracking")
|
||||
logTrackingDebugSnapshot()
|
||||
|
||||
if let summary = trackingLapStats.summaryString() {
|
||||
logDebug("📈 Lap Stats | \(summary)", category: "EyeTracking")
|
||||
}
|
||||
trackingLapStats.reset()
|
||||
}
|
||||
|
||||
private func checkFaceDetectionTimeout() {
|
||||
guard isCameraActive else {
|
||||
stopFaceDetectionTimer()
|
||||
@@ -295,3 +351,45 @@ class EnforceModeService: ObservableObject {
|
||||
isTestMode = false
|
||||
}
|
||||
}
|
||||
|
||||
private struct TrackingLapStats {
|
||||
private var faceWidthValues: [Double] = []
|
||||
private var horizontalValues: [Double] = []
|
||||
private var verticalValues: [Double] = []
|
||||
|
||||
mutating func ingest(faceWidth: Double?, horizontal: Double?, vertical: Double?) {
|
||||
if let faceWidth { faceWidthValues.append(faceWidth) }
|
||||
if let horizontal { horizontalValues.append(horizontal) }
|
||||
if let vertical { verticalValues.append(vertical) }
|
||||
}
|
||||
|
||||
mutating func reset() {
|
||||
faceWidthValues.removeAll(keepingCapacity: true)
|
||||
horizontalValues.removeAll(keepingCapacity: true)
|
||||
verticalValues.removeAll(keepingCapacity: true)
|
||||
}
|
||||
|
||||
func summaryString() -> String? {
|
||||
guard !faceWidthValues.isEmpty || !horizontalValues.isEmpty || !verticalValues.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let faceWidth = stats(for: faceWidthValues)
|
||||
let horizontal = stats(for: horizontalValues)
|
||||
let vertical = stats(for: verticalValues)
|
||||
|
||||
return "faceWidth[\(faceWidth)] h[\(horizontal)] v[\(vertical)]"
|
||||
}
|
||||
|
||||
private func stats(for values: [Double]) -> String {
|
||||
guard let minValue = values.min(), let maxValue = values.max(), !values.isEmpty else {
|
||||
return "-"
|
||||
}
|
||||
let mean = values.reduce(0, +) / Double(values.count)
|
||||
return "min=\(format(minValue)) max=\(format(maxValue)) mean=\(format(mean))"
|
||||
}
|
||||
|
||||
private func format(_ value: Double) -> String {
|
||||
String(format: "%.3f", value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class EyeTrackingService: NSObject, ObservableObject {
|
||||
private let cameraManager = CameraSessionManager()
|
||||
private let visionPipeline = VisionPipeline()
|
||||
private let processor: VisionGazeProcessor
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var previewLayer: AVCaptureVideoPreviewLayer? {
|
||||
cameraManager.previewLayer
|
||||
@@ -33,6 +34,41 @@ class EyeTrackingService: NSObject, ObservableObject {
|
||||
self.processor = VisionGazeProcessor(config: config)
|
||||
super.init()
|
||||
cameraManager.delegate = self
|
||||
setupSettingsObserver()
|
||||
}
|
||||
|
||||
private func setupSettingsObserver() {
|
||||
SettingsManager.shared._settingsSubject
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] settings in
|
||||
self?.applyStrictness(settings.enforceModeStrictness)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
applyStrictness(SettingsManager.shared.settings.enforceModeStrictness)
|
||||
}
|
||||
|
||||
private func applyStrictness(_ strictness: Double) {
|
||||
let config = TrackingConfig(
|
||||
horizontalAwayThreshold: 0.08,
|
||||
verticalAwayThreshold: 0.12,
|
||||
minBaselineSamples: TrackingConfig.default.minBaselineSamples,
|
||||
baselineSmoothing: TrackingConfig.default.baselineSmoothing,
|
||||
baselineUpdateThreshold: TrackingConfig.default.baselineUpdateThreshold,
|
||||
minConfidence: TrackingConfig.default.minConfidence,
|
||||
eyeClosedThreshold: TrackingConfig.default.eyeClosedThreshold,
|
||||
baselineEnabled: TrackingConfig.default.baselineEnabled,
|
||||
defaultCenterHorizontal: TrackingConfig.default.defaultCenterHorizontal,
|
||||
defaultCenterVertical: TrackingConfig.default.defaultCenterVertical,
|
||||
faceWidthSmoothing: TrackingConfig.default.faceWidthSmoothing,
|
||||
faceWidthScaleMin: TrackingConfig.default.faceWidthScaleMin,
|
||||
faceWidthScaleMax: 1.4,
|
||||
eyeBoundsHorizontalPadding: TrackingConfig.default.eyeBoundsHorizontalPadding,
|
||||
eyeBoundsVerticalPaddingUp: TrackingConfig.default.eyeBoundsVerticalPaddingUp,
|
||||
eyeBoundsVerticalPaddingDown: TrackingConfig.default.eyeBoundsVerticalPaddingDown
|
||||
)
|
||||
|
||||
processor.updateConfig(config)
|
||||
}
|
||||
|
||||
func startEyeTracking() async throws {
|
||||
|
||||
@@ -35,17 +35,59 @@ public struct EyeTrackingDebugState: Sendable {
|
||||
public let leftPupil: CGPoint?
|
||||
public let rightPupil: CGPoint?
|
||||
public let imageSize: CGSize?
|
||||
public let faceWidthRatio: Double?
|
||||
public let normalizedHorizontal: Double?
|
||||
public let normalizedVertical: Double?
|
||||
|
||||
public static let empty = EyeTrackingDebugState(
|
||||
leftEyeRect: nil,
|
||||
rightEyeRect: nil,
|
||||
leftPupil: nil,
|
||||
rightPupil: nil,
|
||||
imageSize: nil
|
||||
imageSize: nil,
|
||||
faceWidthRatio: nil,
|
||||
normalizedHorizontal: nil,
|
||||
normalizedVertical: nil
|
||||
)
|
||||
}
|
||||
|
||||
public struct TrackingConfig: Sendable {
|
||||
public init(
|
||||
horizontalAwayThreshold: Double,
|
||||
verticalAwayThreshold: Double,
|
||||
minBaselineSamples: Int,
|
||||
baselineSmoothing: Double,
|
||||
baselineUpdateThreshold: Double,
|
||||
minConfidence: Double,
|
||||
eyeClosedThreshold: Double,
|
||||
baselineEnabled: Bool,
|
||||
defaultCenterHorizontal: Double,
|
||||
defaultCenterVertical: Double,
|
||||
faceWidthSmoothing: Double,
|
||||
faceWidthScaleMin: Double,
|
||||
faceWidthScaleMax: Double,
|
||||
eyeBoundsHorizontalPadding: Double,
|
||||
eyeBoundsVerticalPaddingUp: Double,
|
||||
eyeBoundsVerticalPaddingDown: Double
|
||||
) {
|
||||
self.horizontalAwayThreshold = horizontalAwayThreshold
|
||||
self.verticalAwayThreshold = verticalAwayThreshold
|
||||
self.minBaselineSamples = minBaselineSamples
|
||||
self.baselineSmoothing = baselineSmoothing
|
||||
self.baselineUpdateThreshold = baselineUpdateThreshold
|
||||
self.minConfidence = minConfidence
|
||||
self.eyeClosedThreshold = eyeClosedThreshold
|
||||
self.baselineEnabled = baselineEnabled
|
||||
self.defaultCenterHorizontal = defaultCenterHorizontal
|
||||
self.defaultCenterVertical = defaultCenterVertical
|
||||
self.faceWidthSmoothing = faceWidthSmoothing
|
||||
self.faceWidthScaleMin = faceWidthScaleMin
|
||||
self.faceWidthScaleMax = faceWidthScaleMax
|
||||
self.eyeBoundsHorizontalPadding = eyeBoundsHorizontalPadding
|
||||
self.eyeBoundsVerticalPaddingUp = eyeBoundsVerticalPaddingUp
|
||||
self.eyeBoundsVerticalPaddingDown = eyeBoundsVerticalPaddingDown
|
||||
}
|
||||
|
||||
public let horizontalAwayThreshold: Double
|
||||
public let verticalAwayThreshold: Double
|
||||
public let minBaselineSamples: Int
|
||||
@@ -56,10 +98,16 @@ public struct TrackingConfig: Sendable {
|
||||
public let baselineEnabled: Bool
|
||||
public let defaultCenterHorizontal: Double
|
||||
public let defaultCenterVertical: Double
|
||||
public let faceWidthSmoothing: Double
|
||||
public let faceWidthScaleMin: Double
|
||||
public let faceWidthScaleMax: Double
|
||||
public let eyeBoundsHorizontalPadding: Double
|
||||
public let eyeBoundsVerticalPaddingUp: Double
|
||||
public let eyeBoundsVerticalPaddingDown: Double
|
||||
|
||||
public static let `default` = TrackingConfig(
|
||||
horizontalAwayThreshold: 0.12,
|
||||
verticalAwayThreshold: 0.18,
|
||||
horizontalAwayThreshold: 0.08,
|
||||
verticalAwayThreshold: 0.12,
|
||||
minBaselineSamples: 8,
|
||||
baselineSmoothing: 0.15,
|
||||
baselineUpdateThreshold: 0.08,
|
||||
@@ -67,6 +115,12 @@ public struct TrackingConfig: Sendable {
|
||||
eyeClosedThreshold: 0.18,
|
||||
baselineEnabled: true,
|
||||
defaultCenterHorizontal: 0.5,
|
||||
defaultCenterVertical: 0.5
|
||||
defaultCenterVertical: 0.5,
|
||||
faceWidthSmoothing: 0.12,
|
||||
faceWidthScaleMin: 0.85,
|
||||
faceWidthScaleMax: 1.4,
|
||||
eyeBoundsHorizontalPadding: 0.1,
|
||||
eyeBoundsVerticalPaddingUp: 0.9,
|
||||
eyeBoundsVerticalPaddingDown: 0.4
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ final class VisionGazeProcessor: @unchecked Sendable {
|
||||
}
|
||||
|
||||
private let baselineModel = GazeBaselineModel()
|
||||
private var faceWidthBaseline: Double?
|
||||
private var faceWidthSmoothed: Double?
|
||||
private var config: TrackingConfig
|
||||
|
||||
init(config: TrackingConfig) {
|
||||
@@ -42,6 +44,8 @@ final class VisionGazeProcessor: @unchecked Sendable {
|
||||
|
||||
func resetBaseline() {
|
||||
baselineModel.reset()
|
||||
faceWidthBaseline = nil
|
||||
faceWidthSmoothed = nil
|
||||
}
|
||||
|
||||
func process(analysis: VisionPipeline.FaceAnalysis) -> ObservationResult {
|
||||
@@ -84,13 +88,16 @@ final class VisionGazeProcessor: @unchecked Sendable {
|
||||
|
||||
let eyesClosed = detectEyesClosed(left: leftEye, right: rightEye)
|
||||
let (horizontal, vertical) = normalizePupilPosition(left: leftEye, right: rightEye)
|
||||
let faceWidthRatio = Double(face.boundingBox.size.width)
|
||||
let distanceScale = updateDistanceScale(faceWidthRatio: faceWidthRatio)
|
||||
|
||||
let confidence = calculateConfidence(leftEye: leftEye, rightEye: rightEye)
|
||||
let gazeState = decideGazeState(
|
||||
horizontal: horizontal,
|
||||
vertical: vertical,
|
||||
confidence: confidence,
|
||||
eyesClosed: eyesClosed
|
||||
eyesClosed: eyesClosed,
|
||||
distanceScale: distanceScale
|
||||
)
|
||||
|
||||
let debugState = EyeTrackingDebugState(
|
||||
@@ -98,7 +105,10 @@ final class VisionGazeProcessor: @unchecked Sendable {
|
||||
rightEyeRect: rightEye?.frame,
|
||||
leftPupil: leftEye?.pupil,
|
||||
rightPupil: rightEye?.pupil,
|
||||
imageSize: analysis.imageSize
|
||||
imageSize: analysis.imageSize,
|
||||
faceWidthRatio: faceWidthRatio,
|
||||
normalizedHorizontal: horizontal,
|
||||
normalizedVertical: vertical
|
||||
)
|
||||
|
||||
return ObservationResult(
|
||||
@@ -132,10 +142,17 @@ final class VisionGazeProcessor: @unchecked Sendable {
|
||||
pupilPoint = bounds.center
|
||||
}
|
||||
|
||||
let paddedFrame = expandRect(
|
||||
CGRect(x: bounds.minX, y: bounds.minY, width: bounds.size.width, height: bounds.size.height),
|
||||
horizontalPadding: config.eyeBoundsHorizontalPadding,
|
||||
verticalPaddingUp: config.eyeBoundsVerticalPaddingUp,
|
||||
verticalPaddingDown: config.eyeBoundsVerticalPaddingDown
|
||||
)
|
||||
|
||||
let normalizedPupil: CGPoint?
|
||||
if let pupilPoint {
|
||||
let nx = clamp((pupilPoint.x - bounds.minX) / bounds.size.width)
|
||||
let ny = clamp((pupilPoint.y - bounds.minY) / bounds.size.height)
|
||||
let nx = clamp((pupilPoint.x - paddedFrame.minX) / paddedFrame.size.width)
|
||||
let ny = clamp((pupilPoint.y - paddedFrame.minY) / paddedFrame.size.height)
|
||||
normalizedPupil = CGPoint(x: nx, y: ny)
|
||||
} else {
|
||||
normalizedPupil = nil
|
||||
@@ -146,7 +163,7 @@ final class VisionGazeProcessor: @unchecked Sendable {
|
||||
width: bounds.size.width,
|
||||
height: bounds.size.height,
|
||||
pupil: pupilPoint,
|
||||
frame: CGRect(x: bounds.minX, y: bounds.minY, width: bounds.size.width, height: bounds.size.height),
|
||||
frame: paddedFrame,
|
||||
normalizedPupil: normalizedPupil,
|
||||
hasPupilLandmarks: hasPupilLandmarks
|
||||
)
|
||||
@@ -204,6 +221,23 @@ final class VisionGazeProcessor: @unchecked Sendable {
|
||||
min(1, max(0, value))
|
||||
}
|
||||
|
||||
private func expandRect(
|
||||
_ rect: CGRect,
|
||||
horizontalPadding: Double,
|
||||
verticalPaddingUp: Double,
|
||||
verticalPaddingDown: Double
|
||||
) -> CGRect {
|
||||
let dx = rect.width * CGFloat(horizontalPadding)
|
||||
let up = rect.height * CGFloat(verticalPaddingUp)
|
||||
let down = rect.height * CGFloat(verticalPaddingDown)
|
||||
return CGRect(
|
||||
x: rect.origin.x - dx,
|
||||
y: rect.origin.y - up,
|
||||
width: rect.width + (dx * 2),
|
||||
height: rect.height + up + down
|
||||
)
|
||||
}
|
||||
|
||||
private func averageCoordinate(left: CGFloat?, right: CGFloat?, fallback: Double?) -> Double? {
|
||||
switch (left, right) {
|
||||
case let (left?, right?):
|
||||
@@ -258,7 +292,8 @@ final class VisionGazeProcessor: @unchecked Sendable {
|
||||
horizontal: Double?,
|
||||
vertical: Double?,
|
||||
confidence: Double,
|
||||
eyesClosed: Bool
|
||||
eyesClosed: Bool,
|
||||
distanceScale: Double
|
||||
) -> GazeState {
|
||||
guard confidence >= config.minConfidence else { return .unknown }
|
||||
guard let horizontal, let vertical else { return .unknown }
|
||||
@@ -271,7 +306,21 @@ final class VisionGazeProcessor: @unchecked Sendable {
|
||||
|
||||
let deltaH = abs(horizontal - baseline.horizontal)
|
||||
let deltaV = abs(vertical - baseline.vertical)
|
||||
let away = deltaH > config.horizontalAwayThreshold || deltaV > config.verticalAwayThreshold
|
||||
let thresholdH = config.horizontalAwayThreshold * distanceScale
|
||||
let thresholdV = config.verticalAwayThreshold * distanceScale
|
||||
|
||||
let lookingDown = vertical > baseline.vertical
|
||||
let lookingUp = vertical < baseline.vertical
|
||||
let verticalMultiplier: Double
|
||||
if lookingDown {
|
||||
verticalMultiplier = 1.2
|
||||
} else if lookingUp {
|
||||
verticalMultiplier = 1.8
|
||||
} else {
|
||||
verticalMultiplier = 1.0
|
||||
}
|
||||
let verticalAway = deltaV > (thresholdV * verticalMultiplier)
|
||||
let away = deltaH > thresholdH || verticalAway
|
||||
|
||||
if config.baselineEnabled {
|
||||
if baseline.sampleCount < config.minBaselineSamples {
|
||||
@@ -294,4 +343,28 @@ final class VisionGazeProcessor: @unchecked Sendable {
|
||||
if !stable { return .unknown }
|
||||
return away ? .lookingAway : .lookingAtScreen
|
||||
}
|
||||
|
||||
private func updateDistanceScale(faceWidthRatio: Double) -> Double {
|
||||
let smoothed: Double
|
||||
if let existing = faceWidthSmoothed {
|
||||
smoothed = existing + (faceWidthRatio - existing) * config.faceWidthSmoothing
|
||||
} else {
|
||||
smoothed = faceWidthRatio
|
||||
}
|
||||
faceWidthSmoothed = smoothed
|
||||
|
||||
if faceWidthBaseline == nil {
|
||||
faceWidthBaseline = smoothed
|
||||
return 1.0
|
||||
}
|
||||
|
||||
let baseline = faceWidthBaseline ?? smoothed
|
||||
guard baseline > 0 else { return 1.0 }
|
||||
let ratio = baseline / max(0.0001, smoothed)
|
||||
return clampDouble(ratio, min: config.faceWidthScaleMin, max: config.faceWidthScaleMax)
|
||||
}
|
||||
|
||||
private func clampDouble(_ value: Double, min: Double, max: Double) -> Double {
|
||||
Swift.min(max, Swift.max(min, value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,12 @@ struct EnforceModeSetupContent: View {
|
||||
eyeTrackingStatusView
|
||||
trackingConstantsView
|
||||
}
|
||||
if enforceModeService.isEnforceModeEnabled {
|
||||
strictnessControlView
|
||||
}
|
||||
if enforceModeService.isCameraActive {
|
||||
trackingLapButton
|
||||
}
|
||||
privacyInfoView
|
||||
}
|
||||
|
||||
@@ -330,9 +336,76 @@ struct EnforceModeSetupContent: View {
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if let faceWidth = eyeTrackingService.debugState.faceWidthRatio {
|
||||
HStack(spacing: 12) {
|
||||
Text("Face Width:")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(String(format: "%.3f", faceWidth))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if let horizontal = eyeTrackingService.debugState.normalizedHorizontal,
|
||||
let vertical = eyeTrackingService.debugState.normalizedVertical {
|
||||
HStack(spacing: 12) {
|
||||
Text("Ratios:")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("H \(String(format: "%.3f", horizontal))")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("V \(String(format: "%.3f", vertical))")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(sectionPadding)
|
||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: sectionCornerRadius))
|
||||
}
|
||||
|
||||
private var strictnessControlView: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Tracking Strictness")
|
||||
.font(headerFont)
|
||||
|
||||
Slider(
|
||||
value: $settingsManager.settings.enforceModeStrictness,
|
||||
in: 0...1
|
||||
)
|
||||
.controlSize(.small)
|
||||
|
||||
HStack {
|
||||
Text("Lenient")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Text("Strict")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(sectionPadding)
|
||||
.glassEffectIfAvailable(GlassStyle.regular, in: .rect(cornerRadius: sectionCornerRadius))
|
||||
}
|
||||
|
||||
private var trackingLapButton: some View {
|
||||
Button(action: {
|
||||
enforceModeService.logTrackingLap()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "flag.checkered")
|
||||
Text("Lap Marker")
|
||||
.font(.headline)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.regular)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user