MCPcopy Create free account
hub / github.com/FastLED/FastLED / project_to_hull

Function project_to_hull

src/fl/gfx/rgbw_colorimetric.cpp.hpp:183–244  ·  view source on GitHub ↗

Project an out-of-hull target XYZ onto the achievable LED gamut (#2708, math-model gist §3). Returns true if a projection was applied, in which case `X_t` and `xy_t` are updated to the projected point at unit Y. When the target is already inside the full RGB triangle, returns false and leaves `X_t` / `xy_t` untouched. Algorithm matches the reference's `_strict_project_target_xyz_to_led_hull`: run

Source from the content-addressed store, hash-verified

181// 1.0 per sub-gamut, then pick the sub-gamut with smallest residual. Use
182// the achieved chromaticity as the new target xy.
183static bool project_to_hull(const ProfileCache& cache,
184 float X_t[3], float xy_t[2]) FL_NOEXCEPT {
185 const float sum = X_t[0] + X_t[1] + X_t[2];
186 if (sum < 1e-12f) return false;
187 xy_t[0] = X_t[0] / sum;
188 xy_t[1] = X_t[1] / sum;
189
190 const DiodeProfile& p = *cache.profile;
191 float bary[3];
192 // Test the full RGB triangle (not the sub-gamut triangles) — a target
193 // can be outside a single sub-gamut yet still inside the full hull.
194 if (barycentric_xy(xy_t, p.xy_r, p.xy_g, p.xy_b, bary)
195 && bary[0] >= -1e-9f && bary[1] >= -1e-9f && bary[2] >= -1e-9f) {
196 return false;
197 }
198
199 struct Tri { const float* a; const float* b; const float* c; };
200 const Tri tris[3] = {
201 { cache.P_R, cache.P_G, cache.P_W },
202 { cache.P_R, cache.P_B, cache.P_W },
203 { cache.P_B, cache.P_G, cache.P_W },
204 };
205 float best_xyz[3] = {0, 0, 0};
206 float best_residual = 1e30f;
207 for (int k = 0; k < 3; ++k) {
208 const Tri& tri = tris[k];
209 const float M[3][3] = {
210 { tri.a[0], tri.b[0], tri.c[0] },
211 { tri.a[1], tri.b[1], tri.c[1] },
212 { tri.a[2], tri.b[2], tri.c[2] },
213 };
214 float t[3], residual;
215 nnls3(M, X_t, t, &residual);
216 // Cap drive at full-scale per sub-gamut, matching the reference.
217 float mt = t[0];
218 if (t[1] > mt) mt = t[1];
219 if (t[2] > mt) mt = t[2];
220 if (mt > 1.0f) { const float inv = 1.0f / mt; t[0] *= inv; t[1] *= inv; t[2] *= inv; }
221 float xyz[3];
222 xyz[0] = M[0][0]*t[0] + M[0][1]*t[1] + M[0][2]*t[2];
223 xyz[1] = M[1][0]*t[0] + M[1][1]*t[1] + M[1][2]*t[2];
224 xyz[2] = M[2][0]*t[0] + M[2][1]*t[1] + M[2][2]*t[2];
225 if (xyz[1] <= 1e-12f) continue;
226 if (residual < best_residual) {
227 best_residual = residual;
228 best_xyz[0] = xyz[0]; best_xyz[1] = xyz[1]; best_xyz[2] = xyz[2];
229 }
230 }
231 if (best_xyz[1] <= 1e-12f) {
232 // All sub-gamut projections collapsed to zero — pathological
233 // profile or extreme target. Leave the original X_t alone; the
234 // strict solver will return false and the dispatch falls back.
235 return false;
236 }
237 const float s2 = best_xyz[0] + best_xyz[1] + best_xyz[2];
238 xy_t[0] = best_xyz[0] / s2;
239 xy_t[1] = best_xyz[1] / s2;
240 // Per reference, normalize Y to 1.0 for downstream routing. The full-chroma

Callers 4

solve_strict_subgamut_xyFunction · 0.70
solve_wx_lp_legacyFunction · 0.70
solve_wx_overdriveFunction · 0.70

Calls 3

barycentric_xyFunction · 0.85
xyY_to_XYZFunction · 0.85
nnls3Function · 0.70

Tested by

no test coverage detected