import Foundation import ForgeColor /// Flattened look ready for camera push: optional native CDL + baked 3D LUT. /// When cdl is present, camera applies CDL first, then LUT (split mode). public struct FlattenedLook: Sendable { public var cdl: CDL? public var lut: Lut3D? public var latticeSize: Int public init(cdl: CDL?, lut: Lut3D?, latticeSize: Int) { self.cdl = cdl self.lut = lut self.latticeSize = latticeSize } } public enum Flattener { /// Bake a grade stack to a single 3D LUT over the given lattice. /// splitLeadingCDL: if stack's first enabled-relevant node is a CDL, keep it /// native (fast camera trim) and bake only the remainder. Requires the camera /// to apply CDL before LUT. LUT input domain matches whatever the stack input /// domain is (camera log signal in production use). public static func flatten(stack: GradeStack, latticeSize: Int, splitLeadingCDL: Bool) -> FlattenedLook { var nodes = stack.nodes var splitCDL: CDL? if splitLeadingCDL, let first = nodes.first, first.isEnabled, case .cdl(let cdl) = first.kind { splitCDL = cdl nodes.removeFirst() } let remainder = GradeStack(nodes: nodes) var lut: Lut3D if let cdl = splitCDL { // LUT domain = post-CDL signal. Camera applies CDL then LUT, so bake // remainder over the *raw* lattice but pre-invert nothing: lattice point p // represents CDL output. remainder(p) is exactly what LUT must produce. lut = Lut3D.build(size: latticeSize) { remainder.evaluate($0) } _ = cdl } else { lut = Lut3D.build(size: latticeSize) { remainder.evaluate($0) } } return FlattenedLook(cdl: splitCDL, lut: lut, latticeSize: latticeSize) } }