OpticsControls.swift (6621B)
1 import AppKit 2 3 struct OpticalSettings: Equatable, Codable { 4 enum Pattern: Int, Codable { case scene, whiteGrid, greenGrid } 5 var pattern: Pattern = .scene 6 var redGain: Float = 1 7 var blueHorizontalGain: Float = 1 8 var blueVerticalGain: Float = 1 9 10 static var cacheURL: URL { HeadsetOptics.cacheURL.deletingLastPathComponent().appendingPathComponent("optics-adjustments.json") } 11 static func initial() -> OpticalSettings { 12 var settings = (try? JSONDecoder().decode(OpticalSettings.self, from: Data(contentsOf: cacheURL))) ?? OpticalSettings() 13 settings.redGain = settings.redGain.isFinite ? min(4, max(-1, settings.redGain)) : 1 14 settings.blueHorizontalGain = settings.blueHorizontalGain.isFinite ? min(4, max(-1, settings.blueHorizontalGain)) : 1 15 settings.blueVerticalGain = settings.blueVerticalGain.isFinite ? min(4, max(-1, settings.blueVerticalGain)) : 1 16 if CommandLine.arguments.contains("--green-grid") { settings.pattern = .greenGrid } 17 else if CommandLine.arguments.contains("--optics-grid") { settings.pattern = .whiteGrid } 18 return settings 19 } 20 21 func save() { 22 do { try JSONEncoder().encode(self).write(to: Self.cacheURL, options: .atomic) } 23 catch { log("cannot save optical adjustments: \(error)") } 24 } 25 } 26 27 /// Main-screen controls; the render worker receives settings through Renderer's lock. 28 final class OpticsControls: NSObject { 29 private let renderer: Renderer 30 private let window: NSWindow 31 private let pattern = NSPopUpButton() 32 private let red = NSSlider(value: 1, minValue: -1, maxValue: 4, target: nil, action: nil) 33 private let blueHorizontal = NSSlider(value: 1, minValue: -1, maxValue: 4, target: nil, action: nil) 34 private let blueVertical = NSSlider(value: 1, minValue: -1, maxValue: 4, target: nil, action: nil) 35 private let redLabel = NSTextField(labelWithString: "") 36 private var saveTimer: Timer? 37 private let blueHorizontalLabel = NSTextField(labelWithString: "") 38 39 private let blueVerticalLabel = NSTextField(labelWithString: "") 40 41 init(renderer: Renderer) { 42 self.renderer = renderer 43 window = NSWindow(contentRect: NSRect(x: 60, y: 100, width: 460, height: 340), 44 styleMask: [.titled, .miniaturizable], backing: .buffered, defer: false) 45 super.init() 46 window.title = "Headset optics" 47 window.isReleasedWhenClosed = false 48 let stack = NSStackView() 49 stack.orientation = .vertical; stack.alignment = .leading; stack.spacing = 12 50 stack.edgeInsets = NSEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) 51 stack.translatesAutoresizingMaskIntoConstraints = false 52 window.contentView!.addSubview(stack) 53 NSLayoutConstraint.activate([ 54 stack.leadingAnchor.constraint(equalTo: window.contentView!.leadingAnchor), 55 stack.trailingAnchor.constraint(equalTo: window.contentView!.trailingAnchor), 56 stack.topAnchor.constraint(equalTo: window.contentView!.topAnchor) 57 ]) 58 pattern.addItems(withTitles: ["3D scene", "White grid — sharpness and color", "Green grid — sharpness only"]) 59 pattern.target = self; pattern.action = #selector(changed) 60 stack.addArrangedSubview(pattern) 61 let help = NSTextField(wrappingLabelWithString: "Compare both line directions on the stationary grids. Try one eye at a time; the green cross marks straight ahead.") 62 stack.addArrangedSubview(help) 63 for (slider, label) in [(red, redLabel), (blueHorizontal, blueHorizontalLabel), (blueVertical, blueVerticalLabel)] { 64 slider.target = self; slider.action = #selector(changed) 65 slider.isContinuous = true 66 slider.widthAnchor.constraint(equalToConstant: 410).isActive = true 67 stack.addArrangedSubview(label); stack.addArrangedSubview(slider) 68 } 69 stack.addArrangedSubview(NSTextField(labelWithString: "Color correction: 0% = off, 100% = factory")) 70 let reset = NSButton(title: "Reset colors to factory", target: self, action: #selector(resetColors)) 71 let buttons = NSStackView(views: [reset, NSButton(title: "Quit", target: NSApp, action: #selector(NSApplication.terminate(_:)))]) 72 buttons.spacing = 12 73 stack.addArrangedSubview(buttons) 74 let settings = renderer.opticalSettings() 75 pattern.selectItem(at: settings.pattern.rawValue) 76 red.floatValue = settings.redGain 77 blueHorizontal.floatValue = settings.blueHorizontalGain 78 blueVertical.floatValue = settings.blueVerticalGain 79 refreshLabels() 80 window.setContentSize(NSSize(width: 460, height: max(340, stack.fittingSize.height))) 81 if let screen = NSScreen.screens.first(where: { ($0.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value == CGMainDisplayID() }) { 82 window.setFrameOrigin(NSPoint(x: screen.visibleFrame.minX + 60, y: screen.visibleFrame.maxY - window.frame.height - 60)) 83 } 84 window.makeKeyAndOrderFront(nil) 85 } 86 87 @objc private func changed() { 88 var settings = renderer.opticalSettings() 89 settings.pattern = OpticalSettings.Pattern(rawValue: pattern.indexOfSelectedItem) ?? .scene 90 settings.redGain = (red.floatValue * 100).rounded() / 100 91 settings.blueHorizontalGain = (blueHorizontal.floatValue * 100).rounded() / 100 92 settings.blueVerticalGain = (blueVertical.floatValue * 100).rounded() / 100 93 renderer.setOpticalSettings(settings) 94 saveTimer?.invalidate() 95 saveTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { _ in settings.save() } 96 refreshLabels() 97 } 98 99 private func refreshLabels() { 100 let colorsVisible = pattern.indexOfSelectedItem != OpticalSettings.Pattern.greenGrid.rawValue 101 red.isEnabled = colorsVisible; blueHorizontal.isEnabled = colorsVisible; blueVertical.isEnabled = colorsVisible 102 redLabel.stringValue = String(format: "Red correction: %.0f%%", red.floatValue * 100) 103 blueHorizontalLabel.stringValue = String(format: "Blue horizontal correction (sides): %.0f%%", blueHorizontal.floatValue * 100) 104 blueVerticalLabel.stringValue = String(format: "Blue vertical correction (top/bottom): %.0f%%", blueVertical.floatValue * 100) 105 } 106 107 @objc private func resetColors() { red.floatValue = 1; blueHorizontal.floatValue = 1; blueVertical.floatValue = 1; changed() } 108 func close() { 109 saveTimer?.invalidate(); saveTimer = nil 110 renderer.opticalSettings().save() 111 window.orderOut(nil) 112 } 113 }