Port of `_strict_project_target_xyz_to_led_hull`. When target_xy is outside the LED RGB triangle, projects to the nearest in-hull point by trying NNLS across all three sub-gamuts and picking the one with smallest residual. Returns true if a projection was applied. Updates X_t and xy_t in place.
| 764 | // across all three sub-gamuts and picking the one with smallest residual. |
| 765 | // Returns true if a projection was applied. Updates X_t and xy_t in place. |
| 766 | inline bool project_to_hull(const DiodeProfile& p, float X_t[3], float xy_t[2]) { |
| 767 | const float sum = X_t[0] + X_t[1] + X_t[2]; |
| 768 | if (sum < 1e-12f) return false; |
| 769 | xy_t[0] = X_t[0] / sum; xy_t[1] = X_t[1] / sum; |
| 770 | // Test full RGB triangle containment. |
| 771 | float bary[3]; |
| 772 | if (barycentric_xy(xy_t, p.xy_r, p.xy_g, p.xy_b, bary) && |
| 773 | bary[0] >= -1e-9f && bary[1] >= -1e-9f && bary[2] >= -1e-9f) { |
| 774 | return false; // already in hull |
| 775 | } |
| 776 | // Out-of-hull: NNLS across each sub-gamut, pick min residual. |
| 777 | float P_R[3], P_G[3], P_B[3], P_W[3]; |
| 778 | xyY_to_XYZ(p.xy_r[0], p.xy_r[1], p.lum_r, P_R); |
| 779 | xyY_to_XYZ(p.xy_g[0], p.xy_g[1], p.lum_g, P_G); |
| 780 | xyY_to_XYZ(p.xy_b[0], p.xy_b[1], p.lum_b, P_B); |
| 781 | xyY_to_XYZ(p.xy_w[0], p.xy_w[1], p.lum_w, P_W); |
| 782 | struct Tri { const float* a; const float* b; const float* c; }; |
| 783 | const Tri tris[3] = { |
| 784 | {P_R, P_G, P_W}, {P_R, P_B, P_W}, {P_B, P_G, P_W}, |
| 785 | }; |
| 786 | float best_xyz[3] = {0,0,0}; |
| 787 | float best_residual = 1e30f; |
| 788 | for (const Tri& tri : tris) { |
| 789 | float M[3][3] = { |
| 790 | {tri.a[0], tri.b[0], tri.c[0]}, |
| 791 | {tri.a[1], tri.b[1], tri.c[1]}, |
| 792 | {tri.a[2], tri.b[2], tri.c[2]} |
| 793 | }; |
| 794 | float t[3], res; |
| 795 | nnls3(M, X_t, t, res); |
| 796 | // Cap drive to 1.0 per the reference (`if max_t > 1.0: t /= max_t`). |
| 797 | float mt = t[0]; if (t[1] > mt) mt = t[1]; if (t[2] > mt) mt = t[2]; |
| 798 | if (mt > 1.0f) { float inv = 1.0f/mt; t[0]*=inv; t[1]*=inv; t[2]*=inv; } |
| 799 | float xyz[3]; |
| 800 | xyz[0] = M[0][0]*t[0] + M[0][1]*t[1] + M[0][2]*t[2]; |
| 801 | xyz[1] = M[1][0]*t[0] + M[1][1]*t[1] + M[1][2]*t[2]; |
| 802 | xyz[2] = M[2][0]*t[0] + M[2][1]*t[1] + M[2][2]*t[2]; |
| 803 | if (xyz[1] <= 1e-12f) continue; |
| 804 | if (res < best_residual) { |
| 805 | best_residual = res; |
| 806 | best_xyz[0] = xyz[0]; best_xyz[1] = xyz[1]; best_xyz[2] = xyz[2]; |
| 807 | } |
| 808 | } |
| 809 | if (best_xyz[1] <= 1e-12f) return false; |
| 810 | // Per reference: return achievable xy at unit Y. The full-chroma topology |
| 811 | // is re-solved by the caller. |
| 812 | const float s2 = best_xyz[0] + best_xyz[1] + best_xyz[2]; |
| 813 | xy_t[0] = best_xyz[0] / s2; xy_t[1] = best_xyz[1] / s2; |
| 814 | xyY_to_XYZ(xy_t[0], xy_t[1], 1.0f, X_t); |
| 815 | return true; |
| 816 | } |
| 817 | |
| 818 | // Port of the reference's strict_subgamut: project, then triangle-route. |
| 819 | inline bool strict_with_projection(const DiodeProfile& p, ProfileCache& cache, |
no test coverage detected