import Foundation import ForgeColor /// Tangent Wave control IDs — mirror of the panel's control layout. /// Real IDs assigned by Tangent Hub control map; these are our canonical set, /// remapped at Hub-registration time in the macOS phase. public enum TangentControls { // Trackballs: X/Y per ball. Ball 1 = lift(offset), 2 = gamma(power), 3 = gain(slope). public static let liftBallX: Int = 101 public static let liftBallY: Int = 102 public static let gammaBallX: Int = 111 public static let gammaBallY: Int = 112 public static let gainBallX: Int = 121 public static let gainBallY: Int = 122 // Rings (master per zone). public static let liftRing: Int = 103 public static let gammaRing: Int = 113 public static let gainRing: Int = 123 // Knobs. public static let satKnob: Int = 201 public static let contrastKnob: Int = 202 // Buttons. public static let bypassButton: Int = 301 public static let grabStillButton: Int = 302 public static let nextSlotButton: Int = 303 public static let prevSlotButton: Int = 304 public static let resetButton: Int = 305 } /// Panel input event. public enum PanelInput: Equatable, Sendable { case encoder(id: Int, delta: Int) case button(id: Int, pressed: Bool) } /// CDL parameter targeted by a panel control. public enum CDLParam: String, Equatable, Sendable { case slopeRed, slopeGreen, slopeBlue, slopeMaster case offsetRed, offsetGreen, offsetBlue, offsetMaster case powerRed, powerGreen, powerBlue, powerMaster case saturation case contrast } /// Action produced by mapping a panel input. public enum PanelAction: Equatable, Sendable { case adjustCDL(param: CDLParam, amount: Float) case toggleBypass case grabStill case nextSlot case prevSlot case resetGrade } /// Maps panel controls to actions with per-parameter sensitivity. public struct ControlMap: Sendable { public var encoderMap: [Int: CDLParam] public var buttonMap: [Int: PanelAction] private var sensitivities: [CDLParam: Float] public func sensitivity(for param: CDLParam) -> Float { sensitivities[param] ?? 0.001 } public func action(for input: PanelInput) -> PanelAction? { switch input { case .encoder(let id, let delta): guard let param = encoderMap[id] else { return nil } return .adjustCDL(param: param, amount: Float(delta) * sensitivity(for: param)) case .button(let id, let pressed): guard pressed else { return nil } // act on press only return buttonMap[id] } } /// Default Wave mapping: balls X->red-ish axis, Y->green/blue axis simplification /// (full 2D chromatic ball math lands with real panel calibration). public static let waveDefault = ControlMap( encoderMap: [ TangentControls.liftBallX: .offsetRed, TangentControls.liftBallY: .offsetBlue, TangentControls.liftRing: .offsetMaster, TangentControls.gammaBallX: .powerRed, TangentControls.gammaBallY: .powerBlue, TangentControls.gammaRing: .powerMaster, TangentControls.gainBallX: .slopeRed, TangentControls.gainBallY: .slopeBlue, TangentControls.gainRing: .slopeMaster, TangentControls.satKnob: .saturation, TangentControls.contrastKnob: .contrast, ], buttonMap: [ TangentControls.bypassButton: .toggleBypass, TangentControls.grabStillButton: .grabStill, TangentControls.nextSlotButton: .nextSlot, TangentControls.prevSlotButton: .prevSlot, TangentControls.resetButton: .resetGrade, ], sensitivities: [ .slopeRed: 0.002, .slopeGreen: 0.002, .slopeBlue: 0.002, .slopeMaster: 0.002, .offsetRed: 0.001, .offsetGreen: 0.001, .offsetBlue: 0.001, .offsetMaster: 0.001, .powerRed: 0.002, .powerGreen: 0.002, .powerBlue: 0.002, .powerMaster: 0.002, .saturation: 0.002, .contrast: 0.002, ]) public init(encoderMap: [Int: CDLParam], buttonMap: [Int: PanelAction], sensitivities: [CDLParam: Float]) { self.encoderMap = encoderMap self.buttonMap = buttonMap self.sensitivities = sensitivities } } /// Panel-editable grade state (CDL layer the panel drives). public struct PanelGradeState: Sendable, Equatable { public var cdl: CDL = .identity public var bypassed = false public init() {} public mutating func apply(_ action: PanelAction) { switch action { case .adjustCDL(let param, let amount): switch param { case .slopeRed: cdl.slope.x += amount case .slopeGreen: cdl.slope.y += amount case .slopeBlue: cdl.slope.z += amount case .slopeMaster: cdl.slope += SIMD3(repeating: amount) case .offsetRed: cdl.offset.x += amount case .offsetGreen: cdl.offset.y += amount case .offsetBlue: cdl.offset.z += amount case .offsetMaster: cdl.offset += SIMD3(repeating: amount) case .powerRed: cdl.power.x += amount case .powerGreen: cdl.power.y += amount case .powerBlue: cdl.power.z += amount case .powerMaster: cdl.power += SIMD3(repeating: amount) case .saturation: cdl.saturation = max(0, cdl.saturation + amount) case .contrast: // Contrast pivot 0.435: slope up, offset compensates. cdl.slope += SIMD3(repeating: amount) cdl.offset -= SIMD3(repeating: amount * 0.435) } case .toggleBypass: bypassed.toggle() case .resetGrade: cdl = .identity bypassed = false case .grabStill, .nextSlot, .prevSlot: break // handled by app layer } } /// Short readout string for panel displays. public func displayText(for param: CDLParam) -> String { func t(_ v: Float) -> String { String(format: "%.3f", v) } switch param { case .slopeRed: return "SlpR \(t(cdl.slope.x))" case .slopeGreen: return "SlpG \(t(cdl.slope.y))" case .slopeBlue: return "SlpB \(t(cdl.slope.z))" case .slopeMaster: return "Slp \(t((cdl.slope.x + cdl.slope.y + cdl.slope.z) / 3))" case .offsetRed: return "OffR \(t(cdl.offset.x))" case .offsetGreen: return "OffG \(t(cdl.offset.y))" case .offsetBlue: return "OffB \(t(cdl.offset.z))" case .offsetMaster: return "Off \(t((cdl.offset.x + cdl.offset.y + cdl.offset.z) / 3))" case .powerRed: return "PwrR \(t(cdl.power.x))" case .powerGreen: return "PwrG \(t(cdl.power.y))" case .powerBlue: return "PwrB \(t(cdl.power.z))" case .powerMaster: return "Pwr \(t((cdl.power.x + cdl.power.y + cdl.power.z) / 3))" case .saturation: return "Sat \(t(cdl.saturation))" case .contrast: return "Con \(t(cdl.slope.x))" } } }