-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathtest_prusa_fdm_mixer.cpp
More file actions
214 lines (190 loc) · 7.26 KB
/
Copy pathtest_prusa_fdm_mixer.cpp
File metadata and controls
214 lines (190 loc) · 7.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// test_prusa_fdm_mixer.cpp — verifies C++ implementation matches reference predictions.
//
// Build: g++ -std=c++17 -O2 -Wall -Wextra prusa_fdm_mixer.cpp test_prusa_fdm_mixer.cpp -o test
// Run: ./test
#include "prusa_fdm_mixer.hpp"
#include <cassert>
#include <cmath>
#include <cstdio>
#include <iostream>
#include <vector>
using namespace prusa_fdm_mixer;
struct TestCase {
std::string description;
std::vector<Part> parts;
std::string expected_hex; // optional reference; "" to skip
double tolerance_de; // ΔE tolerance against expected
};
int passed = 0;
int failed = 0;
void check(const std::string& label, bool ok, const std::string& detail = "") {
if (ok) { ++passed; std::printf(" [PASS] %s\n", label.c_str()); }
else { ++failed; std::printf(" [FAIL] %s %s\n", label.c_str(), detail.c_str()); }
}
void test_endpoints() {
std::printf("\n=== Endpoint behavior (gradient safety) ===\n");
// Pure single-color call should return that color.
{
auto r = mix({ { "#007a9d", 1.0 } });
check("pure azure blue (ratio 1.0)", r == "#007a9d", "got " + r);
}
// Two-color with one ratio = 1.0, other = 0.0 should match the dominant.
{
auto r = mix({ { "#ff6338", 1.0 }, { "#252e2e", 0.0 } });
// Endpoint should match #ff6338 within ΔE 1
const LAB ref = rgb_to_lab(hex_to_rgb("#ff6338"));
const LAB got = rgb_to_lab(hex_to_rgb(r));
const double de = delta_e_2000(ref, got);
check("endpoint at ratio 1.0", de < 1.0,
"got " + r + " (ΔE " + std::to_string(de) + ")");
}
{
auto r = mix({ { "#ff6338", 0.0 }, { "#252e2e", 1.0 } });
const LAB ref = rgb_to_lab(hex_to_rgb("#252e2e"));
const LAB got = rgb_to_lab(hex_to_rgb(r));
const double de = delta_e_2000(ref, got);
check("endpoint at ratio 0.0", de < 1.0,
"got " + r + " (ΔE " + std::to_string(de) + ")");
}
}
void test_known_mixes() {
std::printf("\n=== Reference predictions (must match TS port) ===\n");
// These reference outputs were generated by running the same prusa-fdm-mixer model
// in the TypeScript reference implementation. The C++ port must reproduce
// them within ΔE < 1 (rounding tolerance).
struct Ref {
const char* a_hex;
double a_ratio;
const char* b_hex;
double b_ratio;
const char* expected_hex;
};
const Ref refs[] = {
// 50:50 mixes spanning the gamut
{ "#009bc3", 0.5, "#f6b921", 0.5, "#519e5f" }, // cyan + yellow
{ "#c9378c", 0.5, "#f6b921", 0.5, "#cc6545" }, // magenta + yellow
{ "#009bc3", 0.5, "#c9378c", 0.5, "#4a5e94" }, // cyan + magenta
{ "#252e2e", 0.5, "#e4e4e5", 0.5, "#636363" }, // black + white
{ "#007a9d", 0.5, "#b32326", 0.5, "#434446" }, // azure + red
// 75:25 mixes
{ "#007a9d", 0.75, "#f6b921", 0.25, "#1e7c6f" }, // azure-heavy + yellow
{ "#347644", 0.75, "#e2deda", 0.25, "#4f7e5a" }, // green-heavy + white
};
for (const auto& r : refs) {
std::vector<Part> parts = {
{ r.a_hex, r.a_ratio },
{ r.b_hex, r.b_ratio },
};
const std::string got = mix(parts);
const LAB ref_lab = rgb_to_lab(hex_to_rgb(r.expected_hex));
const LAB got_lab = rgb_to_lab(hex_to_rgb(got));
const double de = delta_e_2000(ref_lab, got_lab);
char buf[256];
std::snprintf(buf, sizeof(buf),
"%s @ %.2f + %s @ %.2f -> %s (ref %s, ΔE %.3f)",
r.a_hex, r.a_ratio, r.b_hex, r.b_ratio,
got.c_str(), r.expected_hex, de);
check(buf, de < 1.0);
}
}
void test_three_color() {
std::printf("\n=== 3-color mixes ===\n");
// 1:1:1 of CMY should be a dark mid-tone (predicted, not a specific value)
auto r = mix({
{ "#009bc3", 1.0/3.0 },
{ "#c9378c", 1.0/3.0 },
{ "#f6b921", 1.0/3.0 },
});
const RGB rgb = hex_to_rgb(r);
// Just sanity check: should be a mid-tone, not pure white or pure black
const int sum = rgb.r + rgb.g + rgb.b;
check("CMY 1:1:1 is mid-tone (sum 200..550)",
sum > 200 && sum < 550,
"got " + r + " (sum " + std::to_string(sum) + ")");
// Endpoint check for 3-color: ratio (1, 0, 0)
auto r2 = mix({
{ "#009bc3", 1.0 },
{ "#c9378c", 0.0 },
{ "#f6b921", 0.0 },
});
const LAB ref = rgb_to_lab(hex_to_rgb("#009bc3"));
const LAB got = rgb_to_lab(hex_to_rgb(r2));
const double de = delta_e_2000(ref, got);
check("3-color endpoint (1, 0, 0)", de < 1.0,
"got " + r2 + " (ΔE " + std::to_string(de) + ")");
}
void test_color_space_helpers() {
std::printf("\n=== Color-space round-trips ===\n");
// hex -> rgb -> hex should be identity
const std::vector<std::string> cases = {
"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff",
"#aabbcc", "#123456", "#7f7f7f"
};
for (const auto& h : cases) {
const RGB rgb = hex_to_rgb(h);
const std::string back = rgb_to_hex(rgb);
check("hex round-trip " + h, back == h, "got " + back);
}
// hex -> lab -> rgb -> hex should be close (ΔE < 1)
for (const auto& h : cases) {
const RGB rgb = hex_to_rgb(h);
const LAB lab = rgb_to_lab(rgb);
const RGB back_rgb = lab_to_rgb(lab);
const LAB back_lab = rgb_to_lab(back_rgb);
const double de = delta_e_2000(lab, back_lab);
check("LAB round-trip " + h, de < 1.0,
"ΔE " + std::to_string(de));
}
}
void test_delta_e() {
std::printf("\n=== ΔE2000 sanity ===\n");
// Identical colors -> ΔE = 0
{
const LAB a{50.0, 0.0, 0.0};
const double de = delta_e_2000(a, a);
check("identical colors ΔE=0", de < 0.001, "got " + std::to_string(de));
}
// Reference test from CIE: pair from Sharma 2005 paper
// L1=50,a1=2.6772,b1=-79.7751 vs L2=50,a2=0,b2=-82.7485 -> ΔE ≈ 2.0425
{
const LAB a{50.0, 2.6772, -79.7751};
const LAB b{50.0, 0.0, -82.7485};
const double de = delta_e_2000(a, b);
check("Sharma ref pair (expect ~2.04)",
std::abs(de - 2.0425) < 0.01,
"got " + std::to_string(de));
}
}
void test_hex_parsing() {
std::printf("\n=== Hex parsing edge cases ===\n");
// Without leading #
{
const RGB r = hex_to_rgb("aabbcc");
check("parse without # prefix",
r.r == 0xaa && r.g == 0xbb && r.b == 0xcc,
"");
}
// Uppercase
{
const RGB r = hex_to_rgb("#AABBCC");
check("parse uppercase",
r.r == 0xaa && r.g == 0xbb && r.b == 0xcc, "");
}
// Bad input -> throws
bool threw = false;
try { hex_to_rgb("nope"); } catch (const std::invalid_argument&) { threw = true; }
check("invalid hex throws", threw, "");
}
int main() {
std::printf("prusa-fdm-mixer C++ test suite\n");
std::printf("============================\n");
test_color_space_helpers();
test_delta_e();
test_hex_parsing();
test_endpoints();
test_known_mixes();
test_three_color();
std::printf("\n============================\n");
std::printf("Passed: %d Failed: %d\n", passed, failed);
return failed == 0 ? 0 : 1;
}