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()
@@ -22,6 +36,11 @@ struct CameraPreviewView: NSViewRepresentable {
previewLayer.frame = view.bounds previewLayer.frame = view.bounds
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)
@@ -41,14 +60,23 @@ struct CameraPreviewView: NSViewRepresentable {
// Same layer, just update frame // Same layer, just update frame
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) {
view.layer?.borderColor = color.cgColor if showsBorder {
view.layer?.borderWidth = 4 view.layer?.borderColor = color.cgColor
view.layer?.cornerRadius = 12 view.layer?.borderWidth = 4
} 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,19 +30,23 @@ struct EnforceModeCalibrationOverlayView: View {
} }
private var eyeBoxStep: some View { private var eyeBoxStep: some View {
VStack(spacing: 24) { ZStack {
Text("Adjust Eye Box") VStack(spacing: 16) {
.font(.title2) Text("Adjust Eye Box")
.foregroundStyle(.white) .font(.title2)
.foregroundStyle(.white)
Text( Text(
"Use the sliders to fit the boxes around your eyes. When it looks right, continue." "Use the sliders to fit the boxes around your eyes. When it looks right, continue."
) )
.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,24 +65,29 @@ 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)
HStack(spacing: 12) { VStack {
Button("Cancel") { Spacer()
calibrationService.dismissOverlay() HStack(spacing: 12) {
enforceModeService.stopTestMode() Button("Cancel") {
} calibrationService.dismissOverlay()
.buttonStyle(.bordered) enforceModeService.stopTestMode()
}
.buttonStyle(.bordered)
Button("Continue") { Button("Continue") {
calibrationService.advance() calibrationService.advance()
}
.buttonStyle(.borderedProminent)
} }
.buttonStyle(.borderedProminent)
} }
.padding(.bottom, 40)
} }
.padding()
} }
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 {
ZStack {
if let layer = eyeTrackingService.previewLayer {
CameraPreviewView(previewLayer: layer, borderColor: NSColor.systemBlue)
.frame(height: 240)
}
GeometryReader { geometry in
EyeTrackingDebugOverlayView(
debugState: eyeTrackingService.debugState,
viewSize: geometry.size
)
}
}
.frame(height: 240)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
private var targetDot: some View { private var targetDot: some View {
GeometryReader { geometry in GeometryReader { geometry in
let target = calibrationService.currentTarget() let target = calibrationService.currentTarget()
Circle() let center = CGPoint(
.fill(Color.blue) x: geometry.size.width * target.x,
.frame(width: 100, height: 100) y: geometry.size.height * target.y
.position( )
x: geometry.size.width * target.x,
y: geometry.size.height * target.y ZStack {
) Circle()
.overlay( .fill(Color.blue)
Circle() .frame(width: 120, height: 120)
.trim(from: 0, to: CGFloat(calibrationService.countdownProgress))
.stroke(Color.blue.opacity(0.8), lineWidth: 6) Circle()
.frame(width: 140, height: 140) .trim(from: 0, to: CGFloat(calibrationService.countdownProgress))
.rotationEffect(.degrees(-90)) .stroke(Color.blue.opacity(0.8), lineWidth: 8)
.animation(.linear(duration: 0.02), value: calibrationService.countdownProgress) .frame(width: 160, height: 160)
) .rotationEffect(.degrees(-90))
.animation(.linear(duration: 0.02), value: calibrationService.countdownProgress)
}
.position(center)
} }
.ignoresSafeArea() .ignoresSafeArea()
} }
private var countdownRing: some View { private var cameraBackground: some View {
Circle() ZStack {
.trim(from: 0, to: CGFloat(calibrationService.countdownProgress)) if let layer = eyeTrackingService.previewLayer {
.stroke(Color.blue.opacity(0.8), lineWidth: 6) CameraPreviewView(
.frame(width: 120, height: 120) previewLayer: layer,
.rotationEffect(.degrees(-90)) borderColor: .clear,
showsBorder: false,
cornerRadius: 0
)
.opacity(0.5)
}
if calibrationService.currentStep == .eyeBox {
GeometryReader { geometry in
EyeTrackingDebugOverlayView(
debugState: eyeTrackingService.debugState,
viewSize: geometry.size
)
.opacity(0.8)
}
}
Color.black.opacity(0.35)
.ignoresSafeArea()
}
.ignoresSafeArea()
} }
} }