120 lines
4.4 KiB
Swift
120 lines
4.4 KiB
Swift
import XCTest
|
|
@testable import ForgeColor
|
|
|
|
final class Lut3DTests: XCTestCase {
|
|
|
|
// Identity lattice is a no-op within interp tolerance.
|
|
func testIdentityNoOp() {
|
|
let lut = Lut3D.identity(size: 17)
|
|
for px in [SIMD3<Float>(0, 0, 0), SIMD3(1, 1, 1), SIMD3(0.5, 0.25, 0.75), SIMD3(0.18, 0.18, 0.18)] {
|
|
let out = lut.sample(px)
|
|
XCTAssertEqual(out.x, px.x, accuracy: 1e-5)
|
|
XCTAssertEqual(out.y, px.y, accuracy: 1e-5)
|
|
XCTAssertEqual(out.z, px.z, accuracy: 1e-5)
|
|
}
|
|
}
|
|
|
|
// Lattice built from an analytic function: interp matches direct eval on off-lattice points.
|
|
func testTrilinearMatchesDirectEval() {
|
|
// f = gain 0.5 (linear function -> trilinear should be near-exact)
|
|
let f: (SIMD3<Float>) -> SIMD3<Float> = { $0 * 0.5 }
|
|
let lut = Lut3D.build(size: 9, function: f)
|
|
for px in [SIMD3<Float>(0.13, 0.57, 0.91), SIMD3(0.9, 0.05, 0.5)] {
|
|
let out = lut.sample(px)
|
|
let direct = f(px)
|
|
XCTAssertEqual(out.x, direct.x, accuracy: 1e-4)
|
|
XCTAssertEqual(out.y, direct.y, accuracy: 1e-4)
|
|
XCTAssertEqual(out.z, direct.z, accuracy: 1e-4)
|
|
}
|
|
}
|
|
|
|
// Nonlinear function: 33^3 approx within 1/1024 where trilinear error bound
|
|
// (max|f''|·h²/8, h=1/32) actually permits it. pow(x, 0.6) has f''→∞ at 0;
|
|
// at x=0.15, |f''|≈3.4 → bound ≈4e-4. Real flattening runs in log space where
|
|
// curvature is gentle everywhere. Samples in [0.15, 1].
|
|
func testNonlinearApproxWithinTolerance() {
|
|
let f: (SIMD3<Float>) -> SIMD3<Float> = {
|
|
SIMD3(pow($0.x, 2.2), pow($0.y, 1.8), pow($0.z, 0.6))
|
|
}
|
|
let lut = Lut3D.build(size: 33, function: f)
|
|
var rng = SystemRandomNumberGenerator()
|
|
for _ in 0..<200 {
|
|
let px = SIMD3<Float>(
|
|
Float.random(in: 0.15...1, using: &rng),
|
|
Float.random(in: 0.15...1, using: &rng),
|
|
Float.random(in: 0.15...1, using: &rng))
|
|
let out = lut.sample(px)
|
|
let direct = f(px)
|
|
XCTAssertEqual(out.x, direct.x, accuracy: 1.0 / 1024)
|
|
XCTAssertEqual(out.y, direct.y, accuracy: 1.0 / 1024)
|
|
XCTAssertEqual(out.z, direct.z, accuracy: 1.0 / 1024)
|
|
}
|
|
}
|
|
|
|
// Out-of-range input clamps to lattice bounds.
|
|
func testInputClamped() {
|
|
let lut = Lut3D.identity(size: 5)
|
|
let out = lut.sample(SIMD3(-0.5, 1.5, 0.5))
|
|
XCTAssertEqual(out.x, 0, accuracy: 1e-5)
|
|
XCTAssertEqual(out.y, 1, accuracy: 1e-5)
|
|
XCTAssertEqual(out.z, 0.5, accuracy: 1e-5)
|
|
}
|
|
|
|
// MARK: CUBE I/O
|
|
|
|
func testCubeRoundTrip() throws {
|
|
let lut = Lut3D.build(size: 5) { $0 * 0.8 + SIMD3(0.05, 0, 0.1) }
|
|
let text = CubeFile.write(lut, title: "Forge Test")
|
|
let parsed = try CubeFile.parse(text)
|
|
XCTAssertEqual(parsed.size, 5)
|
|
for i in 0..<lut.table.count {
|
|
XCTAssertEqual(parsed.table[i], lut.table[i], accuracy: 1e-6)
|
|
}
|
|
}
|
|
|
|
func testCubeWriteFormat() {
|
|
let lut = Lut3D.identity(size: 2)
|
|
let text = CubeFile.write(lut, title: "T")
|
|
XCTAssertTrue(text.contains("TITLE \"T\""))
|
|
XCTAssertTrue(text.contains("LUT_3D_SIZE 2"))
|
|
// First lattice entry (0,0,0), red fastest.
|
|
XCTAssertTrue(text.contains("0.000000 0.000000 0.000000"))
|
|
XCTAssertTrue(text.hasSuffix("\n"))
|
|
}
|
|
|
|
func testCubeParseSkipsCommentsAndDomain() throws {
|
|
let text = """
|
|
# comment line
|
|
TITLE "X"
|
|
LUT_3D_SIZE 2
|
|
DOMAIN_MIN 0.0 0.0 0.0
|
|
DOMAIN_MAX 1.0 1.0 1.0
|
|
|
|
0 0 0
|
|
1 0 0
|
|
0 1 0
|
|
1 1 0
|
|
0 0 1
|
|
1 0 1
|
|
0 1 1
|
|
1 1 1
|
|
"""
|
|
let lut = try CubeFile.parse(text)
|
|
XCTAssertEqual(lut.size, 2)
|
|
XCTAssertEqual(lut.sample(SIMD3(1, 1, 1)).x, 1, accuracy: 1e-6)
|
|
}
|
|
|
|
func testCubeParseRejectsMissingSize() {
|
|
XCTAssertThrowsError(try CubeFile.parse("0 0 0\n1 1 1\n"))
|
|
}
|
|
|
|
func testCubeParseRejectsWrongEntryCount() {
|
|
let text = "LUT_3D_SIZE 2\n0 0 0\n1 1 1\n" // needs 8 entries, has 2
|
|
XCTAssertThrowsError(try CubeFile.parse(text))
|
|
}
|
|
|
|
func testCubeParseRejectsBadFloat() {
|
|
let text = "LUT_3D_SIZE 2\n" + Array(repeating: "0 0 zebra", count: 8).joined(separator: "\n")
|
|
XCTAssertThrowsError(try CubeFile.parse(text))
|
|
}
|
|
}
|