rainbow-dragon/Tests/ForgeColorTests/CurveTests.swift

76 lines
2.8 KiB
Swift

import XCTest
@testable import ForgeColor
final class CurveTests: XCTestCase {
// Default 2-point identity curve is a no-op.
func testIdentityNoOp() {
let c = Curve.identity
for x: Float in [0, 0.1, 0.18, 0.5, 0.9, 1] {
XCTAssertEqual(c.evaluate(x), x, accuracy: 1e-6)
}
}
// Curve passes exactly through its control points.
func testInterpolatesControlPoints() {
let c = Curve(points: [
.init(x: 0, y: 0),
.init(x: 0.25, y: 0.4),
.init(x: 0.75, y: 0.7),
.init(x: 1, y: 1),
])
XCTAssertEqual(c.evaluate(0.25), 0.4, accuracy: 1e-5)
XCTAssertEqual(c.evaluate(0.75), 0.7, accuracy: 1e-5)
XCTAssertEqual(c.evaluate(0), 0, accuracy: 1e-5)
XCTAssertEqual(c.evaluate(1), 1, accuracy: 1e-5)
}
// Monotonic control points -> monotonic output (no spline overshoot ringing).
func testMonotonicPreserved() {
let c = Curve(points: [
.init(x: 0, y: 0),
.init(x: 0.1, y: 0.5), // steep segment then flat classic overshoot trap
.init(x: 0.2, y: 0.55),
.init(x: 1, y: 1),
])
var prev: Float = -1
for i in 0...1000 {
let y = c.evaluate(Float(i) / 1000)
XCTAssertGreaterThanOrEqual(y, prev - 1e-6, "non-monotonic at x=\(Float(i) / 1000)")
prev = y
}
}
// Input outside domain pins to endpoint values.
func testEndpointPinning() {
let c = Curve(points: [.init(x: 0.2, y: 0.3), .init(x: 0.8, y: 0.9)])
XCTAssertEqual(c.evaluate(0), 0.3, accuracy: 1e-6)
XCTAssertEqual(c.evaluate(-5), 0.3, accuracy: 1e-6)
XCTAssertEqual(c.evaluate(1), 0.9, accuracy: 1e-6)
XCTAssertEqual(c.evaluate(99), 0.9, accuracy: 1e-6)
}
// Points auto-sorted by x.
func testUnsortedPointsSorted() {
let c = Curve(points: [.init(x: 1, y: 1), .init(x: 0, y: 0), .init(x: 0.5, y: 0.6)])
XCTAssertEqual(c.evaluate(0.5), 0.6, accuracy: 1e-5)
}
// CurveSet: master applies to all channels, then per-channel.
func testCurveSetMasterThenChannel() {
var set = CurveSet.identity
set.master = Curve(points: [.init(x: 0, y: 0), .init(x: 1, y: 0.5)]) // halve
set.red = Curve(points: [.init(x: 0, y: 0.1), .init(x: 1, y: 1.1)]) // +0.1
let out = set.apply(SIMD3<Float>(1, 1, 1))
XCTAssertEqual(out.x, 0.6, accuracy: 1e-4) // master 1->0.5, red 0.5->0.6
XCTAssertEqual(out.y, 0.5, accuracy: 1e-4)
XCTAssertEqual(out.z, 0.5, accuracy: 1e-4)
}
func testCurveSetIdentityNoOp() {
let out = CurveSet.identity.apply(SIMD3<Float>(0.3, 0.6, 0.9))
XCTAssertEqual(out.x, 0.3, accuracy: 1e-6)
XCTAssertEqual(out.y, 0.6, accuracy: 1e-6)
XCTAssertEqual(out.z, 0.9, accuracy: 1e-6)
}
}