rainbow-dragon/Sources/ForgePanel/ControlMap.swift

177 lines
7 KiB
Swift
Raw Normal View History

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))"
}
}
}