142 lines
4.3 KiB
Swift
142 lines
4.3 KiB
Swift
import Foundation
|
|
import ForgeColor
|
|
|
|
/// Versioned, deterministic JSON serialization of a GradeStack.
|
|
/// Sorted keys -> byte-stable re-encoding, snapshots diff cleanly.
|
|
public enum GradeSnapshot {
|
|
public static let currentVersion = 1
|
|
|
|
public enum SnapshotError: Error, Equatable {
|
|
case unsupportedVersion(Int)
|
|
case unknownNodeKind(String)
|
|
}
|
|
|
|
public static func encode(_ stack: GradeStack) throws -> Data {
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.sortedKeys]
|
|
return try encoder.encode(StackDTO(stack))
|
|
}
|
|
|
|
public static func decode(_ data: Data) throws -> GradeStack {
|
|
let dto = try JSONDecoder().decode(StackDTO.self, from: data)
|
|
guard dto.version == currentVersion else {
|
|
throw SnapshotError.unsupportedVersion(dto.version)
|
|
}
|
|
return GradeStack(nodes: try dto.nodes.map { try $0.toNode() })
|
|
}
|
|
}
|
|
|
|
// MARK: - DTOs
|
|
|
|
private struct StackDTO: Codable {
|
|
var version: Int
|
|
var nodes: [NodeDTO]
|
|
|
|
init(_ stack: GradeStack) {
|
|
version = GradeSnapshot.currentVersion
|
|
nodes = stack.nodes.map(NodeDTO.init)
|
|
}
|
|
}
|
|
|
|
private struct NodeDTO: Codable {
|
|
var id: UUID
|
|
var isEnabled: Bool
|
|
var kind: KindDTO
|
|
|
|
init(_ node: GradeNode) {
|
|
id = node.id
|
|
isEnabled = node.isEnabled
|
|
kind = KindDTO(node.kind)
|
|
}
|
|
|
|
func toNode() throws -> GradeNode {
|
|
GradeNode(id: id, kind: try kind.toKind(), isEnabled: isEnabled)
|
|
}
|
|
}
|
|
|
|
private struct KindDTO: Codable {
|
|
var type: String
|
|
// Flat optional payloads — only the one matching `type` is set.
|
|
var inputTransform: InputTransform?
|
|
var outputTransform: OutputTransform?
|
|
var cdl: CDLDTO?
|
|
var saturation: Float?
|
|
var curves: CurveSet?
|
|
var lutSize: Int?
|
|
var lutTable: [Float]?
|
|
|
|
init(_ kind: GradeNode.Kind) {
|
|
switch kind {
|
|
case .inputTransform(let t):
|
|
type = "inputTransform"
|
|
inputTransform = t
|
|
case .outputTransform(let t):
|
|
type = "outputTransform"
|
|
outputTransform = t
|
|
case .cdl(let c):
|
|
type = "cdl"
|
|
cdl = CDLDTO(c)
|
|
case .saturation(let s):
|
|
type = "saturation"
|
|
saturation = s
|
|
case .curves(let c):
|
|
type = "curves"
|
|
curves = c
|
|
case .lut3d(let lut):
|
|
type = "lut3d"
|
|
lutSize = lut.size
|
|
lutTable = lut.table
|
|
}
|
|
}
|
|
|
|
func toKind() throws -> GradeNode.Kind {
|
|
switch type {
|
|
case "inputTransform":
|
|
guard let t = inputTransform else { throw GradeSnapshot.SnapshotError.unknownNodeKind(type) }
|
|
return .inputTransform(t)
|
|
case "outputTransform":
|
|
guard let t = outputTransform else { throw GradeSnapshot.SnapshotError.unknownNodeKind(type) }
|
|
return .outputTransform(t)
|
|
case "cdl":
|
|
guard let c = cdl else { throw GradeSnapshot.SnapshotError.unknownNodeKind(type) }
|
|
return .cdl(c.toCDL())
|
|
case "saturation":
|
|
guard let s = saturation else { throw GradeSnapshot.SnapshotError.unknownNodeKind(type) }
|
|
return .saturation(s)
|
|
case "curves":
|
|
guard let c = curves else { throw GradeSnapshot.SnapshotError.unknownNodeKind(type) }
|
|
return .curves(c)
|
|
case "lut3d":
|
|
guard let size = lutSize, let table = lutTable,
|
|
table.count == size * size * size * 3 else {
|
|
throw GradeSnapshot.SnapshotError.unknownNodeKind(type)
|
|
}
|
|
return .lut3d(Lut3D(size: size, table: table))
|
|
default:
|
|
throw GradeSnapshot.SnapshotError.unknownNodeKind(type)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// SIMD3 is not Codable-stable across platforms; explicit arrays.
|
|
private struct CDLDTO: Codable {
|
|
var slope: [Float]
|
|
var offset: [Float]
|
|
var power: [Float]
|
|
var saturation: Float
|
|
|
|
init(_ c: CDL) {
|
|
slope = [c.slope.x, c.slope.y, c.slope.z]
|
|
offset = [c.offset.x, c.offset.y, c.offset.z]
|
|
power = [c.power.x, c.power.y, c.power.z]
|
|
saturation = c.saturation
|
|
}
|
|
|
|
func toCDL() -> CDL {
|
|
CDL(
|
|
slope: SIMD3(slope[0], slope[1], slope[2]),
|
|
offset: SIMD3(offset[0], offset[1], offset[2]),
|
|
power: SIMD3(power[0], power[1], power[2]),
|
|
saturation: saturation)
|
|
}
|
|
}
|