This commit is contained in:
Michael Freno
2026-02-01 02:13:32 -05:00
parent 5ae678ffe8
commit d4adb530e0
3 changed files with 117 additions and 69 deletions

View File

@@ -100,6 +100,13 @@ final class CameraSessionManager: NSObject, ObservableObject {
} }
session.addOutput(output) session.addOutput(output)
if let connection = output.connection(with: .video) {
if connection.isVideoMirroringSupported {
connection.automaticallyAdjustsVideoMirroring = false
connection.isVideoMirrored = true
}
}
self.captureSession = session self.captureSession = session
self.videoOutput = output self.videoOutput = output
} }

View File

@@ -11,6 +11,20 @@ import AVFoundation
struct CameraPreviewView: NSViewRepresentable { struct CameraPreviewView: NSViewRepresentable {
let previewLayer: AVCaptureVideoPreviewLayer let previewLayer: AVCaptureVideoPreviewLayer
let borderColor: NSColor let borderColor: NSColor
let showsBorder: Bool
let cornerRadius: CGFloat
init(
previewLayer: AVCaptureVideoPreviewLayer,
borderColor: NSColor,
showsBorder: Bool = true,
cornerRadius: CGFloat = 12
) {
self.previewLayer = previewLayer
self.borderColor = borderColor
self.showsBorder = showsBorder
self.cornerRadius = cornerRadius
}
func makeNSView(context: Context) -> PreviewContainerView { func makeNSView(context: Context) -> PreviewContainerView {
let view = PreviewContainerView() let view = PreviewContainerView()
@@ -23,6 +37,11 @@ struct CameraPreviewView: NSViewRepresentable {
view.layer?.addSublayer(previewLayer) view.layer?.addSublayer(previewLayer)
} }
if let connection = previewLayer.connection, connection.isVideoMirroringSupported {
connection.automaticallyAdjustsVideoMirroring = false
connection.isVideoMirrored = true
}
updateBorder(view: view, color: borderColor) updateBorder(view: view, color: borderColor)
return view return view
@@ -42,13 +61,22 @@ struct CameraPreviewView: NSViewRepresentable {
previewLayer.frame = nsView.bounds previewLayer.frame = nsView.bounds
} }
if let connection = previewLayer.connection, connection.isVideoMirroringSupported {
connection.automaticallyAdjustsVideoMirroring = false
connection.isVideoMirrored = true
}
updateBorder(view: nsView, color: borderColor) updateBorder(view: nsView, color: borderColor)
} }
private func updateBorder(view: NSView, color: NSColor) { private func updateBorder(view: NSView, color: NSColor) {
if showsBorder {
view.layer?.borderColor = color.cgColor view.layer?.borderColor = color.cgColor
view.layer?.borderWidth = 4 view.layer?.borderWidth = 4
view.layer?.cornerRadius = 12 } else {
view.layer?.borderWidth = 0
}
view.layer?.cornerRadius = cornerRadius
view.layer?.masksToBounds = true view.layer?.masksToBounds = true
} }

View File

@@ -16,8 +16,7 @@ struct EnforceModeCalibrationOverlayView: View {
var body: some View { var body: some View {
ZStack { ZStack {
Color.black.opacity(0.85) cameraBackground
.ignoresSafeArea()
switch calibrationService.currentStep { switch calibrationService.currentStep {
case .eyeBox: case .eyeBox:
@@ -31,7 +30,8 @@ struct EnforceModeCalibrationOverlayView: View {
} }
private var eyeBoxStep: some View { private var eyeBoxStep: some View {
VStack(spacing: 24) { ZStack {
VStack(spacing: 16) {
Text("Adjust Eye Box") Text("Adjust Eye Box")
.font(.title2) .font(.title2)
.foregroundStyle(.white) .foregroundStyle(.white)
@@ -42,8 +42,11 @@ struct EnforceModeCalibrationOverlayView: View {
.font(.callout) .font(.callout)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.foregroundStyle(.white.opacity(0.8)) .foregroundStyle(.white.opacity(0.8))
}
eyePreview .padding(.horizontal, 40)
.padding(.top, 40)
.frame(maxWidth: 520)
.frame(maxWidth: .infinity, alignment: .top)
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Text("Width") Text("Width")
@@ -62,10 +65,14 @@ struct EnforceModeCalibrationOverlayView: View {
in: 0.01...0.10 in: 0.01...0.10
) )
} }
.padding() .padding(16)
.background(.white.opacity(0.1)) .background(.black.opacity(0.6))
.clipShape(RoundedRectangle(cornerRadius: 12)) .clipShape(RoundedRectangle(cornerRadius: 12))
.frame(maxWidth: 420)
.frame(maxHeight: .infinity, alignment: .center)
VStack {
Spacer()
HStack(spacing: 12) { HStack(spacing: 12) {
Button("Cancel") { Button("Cancel") {
calibrationService.dismissOverlay() calibrationService.dismissOverlay()
@@ -79,7 +86,8 @@ struct EnforceModeCalibrationOverlayView: View {
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
} }
} }
.padding() .padding(.bottom, 40)
}
} }
private var targetStep: some View { private var targetStep: some View {
@@ -99,11 +107,10 @@ struct EnforceModeCalibrationOverlayView: View {
} }
.padding() .padding()
.background(Color.black.opacity(0.7)) .background(Color.black.opacity(0.7))
.frame(maxWidth: .infinity, alignment: .top) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
targetDot targetDot
VStack { VStack {
Spacer() Spacer()
HStack(spacing: 12) { HStack(spacing: 12) {
@@ -135,50 +142,56 @@ struct EnforceModeCalibrationOverlayView: View {
} }
} }
private var eyePreview: some View { private var targetDot: some View {
GeometryReader { geometry in
let target = calibrationService.currentTarget()
let center = CGPoint(
x: geometry.size.width * target.x,
y: geometry.size.height * target.y
)
ZStack {
Circle()
.fill(Color.blue)
.frame(width: 120, height: 120)
Circle()
.trim(from: 0, to: CGFloat(calibrationService.countdownProgress))
.stroke(Color.blue.opacity(0.8), lineWidth: 8)
.frame(width: 160, height: 160)
.rotationEffect(.degrees(-90))
.animation(.linear(duration: 0.02), value: calibrationService.countdownProgress)
}
.position(center)
}
.ignoresSafeArea()
}
private var cameraBackground: some View {
ZStack { ZStack {
if let layer = eyeTrackingService.previewLayer { if let layer = eyeTrackingService.previewLayer {
CameraPreviewView(previewLayer: layer, borderColor: NSColor.systemBlue) CameraPreviewView(
.frame(height: 240) previewLayer: layer,
borderColor: .clear,
showsBorder: false,
cornerRadius: 0
)
.opacity(0.5)
} }
if calibrationService.currentStep == .eyeBox {
GeometryReader { geometry in GeometryReader { geometry in
EyeTrackingDebugOverlayView( EyeTrackingDebugOverlayView(
debugState: eyeTrackingService.debugState, debugState: eyeTrackingService.debugState,
viewSize: geometry.size viewSize: geometry.size
) )
.opacity(0.8)
} }
} }
.frame(height: 240)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
private var targetDot: some View { Color.black.opacity(0.35)
GeometryReader { geometry in .ignoresSafeArea()
let target = calibrationService.currentTarget()
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.position(
x: geometry.size.width * target.x,
y: geometry.size.height * target.y
)
.overlay(
Circle()
.trim(from: 0, to: CGFloat(calibrationService.countdownProgress))
.stroke(Color.blue.opacity(0.8), lineWidth: 6)
.frame(width: 140, height: 140)
.rotationEffect(.degrees(-90))
.animation(.linear(duration: 0.02), value: calibrationService.countdownProgress)
)
} }
.ignoresSafeArea() .ignoresSafeArea()
} }
private var countdownRing: some View {
Circle()
.trim(from: 0, to: CGFloat(calibrationService.countdownProgress))
.stroke(Color.blue.opacity(0.8), lineWidth: 6)
.frame(width: 120, height: 120)
.rotationEffect(.degrees(-90))
}
} }