Skip to content

Commit a57f340

Browse files
Shadetailclaude
andcommitted
CIE: spline-smoothed spectral locus; same-frame scope creation for the quad split
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 9829f96 commit a57f340

5 files changed

Lines changed: 64 additions & 13 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ so every release ships with a readable summary.
66

77
## v1.0.3 — 2026-08-16
88

9+
- CIE: the spectral-locus horseshoe is drawn as a smooth spline through the
10+
5 nm table instead of showing straight chords on its fast-sweeping
11+
cyan-green edge.
912
- 4-scope layout: when one column holds only the square scopes (vectorscope,
1013
CIE) and the other only the stretchy ones (waveform, histogram), the
1114
vertical split now shifts automatically so the square column is exactly as

‎src/main.cpp‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,8 @@ static void DrawHoverReadout(const ScopeFrame& probe, const Settings& s, float s
458458

459459
// Quad-cell width that panel i's fixed-aspect scope (vectorscope/CIE) can
460460
// actually use at the given cell height — its graph plus label margins. 0 for
461-
// stretchy scopes that take whatever width they get (and before the panel's
462-
// scope exists, which keeps the split centered for that first frame).
461+
// stretchy scopes that take whatever width they get (and if the scope failed
462+
// to create, which keeps the split centered).
463463
static float IdealCellW(int i, float cellH, const Settings& s) {
464464
IScope* sc = g_panels[i].Scope();
465465
float aspect = sc ? sc->AspectRatio() : 0.0f;
@@ -675,6 +675,9 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, LPWSTR, int) {
675675
// Never below 50% for the stretchy column (narrow windows stay 50/50).
676676
float splitX = -1.0f;
677677
if (count == 4) {
678+
// Materialize any scope-type changes now so the split is computed
679+
// from the scopes actually drawn this frame (no one-frame lag).
680+
for (int i = 0; i < count; ++i) g_panels[i].EnsureScope(g_set.panelScope[i]);
678681
const float pad = 2.0f; // matches LayoutRects
679682
float cellH = (area1.y - scopeArea0.y) * 0.5f - pad;
680683
auto colWidth = [&](int a, int b) { // widest ideal width; 0 = has a stretchy panel

‎src/scope/CIEScope.cpp‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#include "scope/CIEScope.h"
22
#include "scope/ChromaMath.h"
33
#include "util/UiReset.h"
4+
#include <algorithm>
5+
#include <array>
6+
#include <cmath>
47
#include <vector>
58

69
namespace {
@@ -21,6 +24,40 @@ const double kLocus[][2] = {
2124
{0.7334,0.2666},{0.7340,0.2660},{0.7344,0.2656},{0.7347,0.2653},
2225
};
2326
struct Tri { double r[2], g[2], b[2]; ImU32 col; const char* name; };
27+
28+
// Densify the locus with a centripetal Catmull-Rom spline (Barry-Goldman
29+
// form). The 5nm table is exact but its chords show where chromaticity sweeps
30+
// fast per wavelength (the cyan-green upper-left edge); interpolating keeps
31+
// every table point and curves smoothly between them. Built once at startup.
32+
std::vector<std::array<double, 2>> BuildSmoothLocus() {
33+
using P2 = std::array<double, 2>;
34+
const int n = (int)(sizeof(kLocus) / sizeof(kLocus[0]));
35+
const int K = 16; // subdivisions per segment (keeps joints sub-degree even zoomed)
36+
auto P = [&](int i) { i = std::clamp(i, 0, n - 1); return P2{ kLocus[i][0], kLocus[i][1] }; };
37+
auto dist = [](const P2& a, const P2& b) { return std::hypot(a[0] - b[0], a[1] - b[1]); };
38+
auto lerp = [](const P2& a, const P2& b, double u) { return P2{ a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u }; };
39+
std::vector<P2> out;
40+
out.reserve((size_t)(n - 1) * K + 1);
41+
for (int i = 0; i + 1 < n; ++i) {
42+
P2 p0 = P(i - 1), p1 = P(i), p2 = P(i + 1), p3 = P(i + 2);
43+
double t0 = 0;
44+
double t1 = t0 + std::sqrt(dist(p0, p1)); // centripetal: alpha = 0.5
45+
double t2 = t1 + std::sqrt(dist(p1, p2));
46+
double t3 = t2 + std::sqrt(dist(p2, p3));
47+
if (t2 - t1 < 1e-9) { out.push_back(p1); continue; } // duplicate point
48+
for (int k = 0; k < K; ++k) {
49+
double t = t1 + (t2 - t1) * k / K;
50+
P2 A1 = (t1 - t0 > 1e-9) ? lerp(p0, p1, (t - t0) / (t1 - t0)) : p1;
51+
P2 A2 = lerp(p1, p2, (t - t1) / (t2 - t1));
52+
P2 A3 = (t3 - t2 > 1e-9) ? lerp(p2, p3, (t - t2) / (t3 - t2)) : p2;
53+
P2 B1 = (t2 - t0 > 1e-9) ? lerp(A1, A2, (t - t0) / (t2 - t0)) : A2;
54+
P2 B2 = (t3 - t1 > 1e-9) ? lerp(A2, A3, (t - t1) / (t3 - t1)) : A2;
55+
out.push_back(lerp(B1, B2, (t - t1) / (t2 - t1)));
56+
}
57+
}
58+
out.push_back(P(n - 1));
59+
return out;
60+
}
2461
}
2562

2663
// xy -> plot[0,1]^2 matching the shader's MapPoint for the active diagram.
@@ -42,10 +79,11 @@ void CIEScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings& s) {
4279
const ImU32 col = ImGui::GetColorU32(ImVec4(s.graticuleColor.x, s.graticuleColor.y, s.graticuleColor.z, s.graticuleOpacity * 0.8f));
4380
const ImU32 colText = ImGui::GetColorU32(ImVec4(0.82f, 0.82f, 0.82f, std::max(0.5f, s.graticuleOpacity)));
4481

45-
// Spectral locus (closed with the line of purples).
46-
const int n = (int)(sizeof(kLocus) / sizeof(kLocus[0]));
47-
std::vector<ImVec2> loc; loc.reserve(n + 1);
48-
for (int i = 0; i < n; ++i) loc.push_back(XY(kLocus[i][0], kLocus[i][1]));
82+
// Spectral locus, spline-densified (closed with the straight line of
83+
// purples, which is a real straight edge and must not be smoothed).
84+
static const std::vector<std::array<double, 2>> kSmooth = BuildSmoothLocus();
85+
std::vector<ImVec2> loc; loc.reserve(kSmooth.size() + 1);
86+
for (const auto& p : kSmooth) loc.push_back(XY(p[0], p[1]));
4987
loc.push_back(loc.front());
5088
dl->AddPolyline(loc.data(), (int)loc.size(), col, 0, 1.0f);
5189

‎src/scope/ScopePanel.cpp‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44
#include <algorithm>
55
#include <cmath>
66

7+
void ScopePanel::EnsureScope(ScopeType want) {
8+
if (scope_ && type_ == want) return;
9+
if (scope_) scope_->Shutdown();
10+
scope_ = CreateScope(want);
11+
if (scope_ && !scope_->Init(device_)) scope_.reset();
12+
type_ = want;
13+
}
14+
715
void ScopePanel::Draw(int index, const ImVec2& panelP0, const ImVec2& panelP1,
816
const ScopeInput& input, Settings& s, float sdrWhiteNits,
917
const ScopeFrame& probe, float uiBrightness) {
10-
ScopeType want = s.panelScope[index];
11-
if (!scope_ || type_ != want) {
12-
if (scope_) scope_->Shutdown();
13-
scope_ = CreateScope(want);
14-
if (scope_ && !scope_->Init(device_)) scope_.reset();
15-
type_ = want;
16-
}
18+
EnsureScope(s.panelScope[index]);
1719
if (!scope_) return;
1820
if (!input.srcSRV || input.srcW == 0 || input.srcH == 0 || input.cropW <= 0 || input.cropH <= 0) return;
1921

‎src/scope/ScopePanel.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ class ScopePanel {
1111
public:
1212
void Init(ID3D11Device* device) { device_ = device; }
1313

14+
// (Re)create the scope instance if the wanted type changed. Called by
15+
// Draw, and by the host before layout so geometry queries (margins,
16+
// aspect) already see the newly selected scope in the same frame.
17+
void EnsureScope(ScopeType want);
18+
1419
// panelP0/P1: the ImGui screen rect allotted to this panel.
1520
// index: which Settings.zoom/pan slot + panelScope entry to use.
1621
void Draw(int index, const ImVec2& panelP0, const ImVec2& panelP1,

0 commit comments

Comments
 (0)