feat(visualizer): controls row + unified MixVisualizerControlState; 3 inert uniforms wired (P10 W2)
This commit is contained in:
@@ -24,7 +24,7 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
{
|
||||
[Inject] public required IReleaseDataService ReleaseData { get; set; }
|
||||
[Inject] public required IJSRuntime JS { get; set; }
|
||||
[Inject] public required MixVisualizerZoomState ZoomState { get; set; }
|
||||
[Inject] public required MixVisualizerControlState ControlState { get; set; }
|
||||
[Inject] public required ILogger<MixWaveformVisualizer> Logger { get; set; }
|
||||
|
||||
// Live playback + the mix duration come from the cascaded streaming player when present. The
|
||||
@@ -73,7 +73,10 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
private IStreamingPlayerService? _subscribedService;
|
||||
private WaveformProfileDto? _profile;
|
||||
private long? _loadedReleaseId;
|
||||
private bool _hasDatum;
|
||||
|
||||
// Whether we are subscribed to the shared control state's Changed event. The controls row (a
|
||||
// sibling component) mutates ControlState and raises Changed; we push the affected uniforms.
|
||||
private bool _subscribedToControls;
|
||||
|
||||
// The profile reference last sent to the module, plus whether it went with a real duration.
|
||||
// Tracked so a per-tick playback push never re-decodes the (up to ~1.2 MB) datum in JS — we only
|
||||
@@ -84,11 +87,18 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
// Theme last pushed to the module, so we only re-push on an actual change.
|
||||
private bool? _lastIsDark;
|
||||
|
||||
/// <summary>
|
||||
/// Slider position in [0, 1]. 0 = most zoomed-out (MaxVisibleSeconds), 1 = most zoomed-in
|
||||
/// (MinVisibleSeconds). Derived from the session-persisted seconds via the log mapping below.
|
||||
/// </summary>
|
||||
private double ZoomFraction => MixZoomMapping.SecondsToFraction(ZoomState.VisibleSeconds);
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// Subscribe once to the shared control state. The controls row mutates it and raises Changed;
|
||||
// we are the sole owner of the JS module handle, so we do the uniform pushes here. This keeps
|
||||
// the handle single-owned (no handle sharing, no service-locator) — the scoped state object is
|
||||
// the decoupling seam between the foreground controls and this backdrop bridge.
|
||||
if (!_subscribedToControls)
|
||||
{
|
||||
ControlState.Changed += OnControlStateChanged;
|
||||
_subscribedToControls = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
@@ -119,7 +129,6 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
if (result is { Success: true, Value: { } profile } && profile.BucketCount > 0 && profile.Data.Length > 0)
|
||||
{
|
||||
_profile = profile;
|
||||
_hasDatum = true;
|
||||
DebugLog($"datum fetch OK — {profile.BucketCount} buckets, base64 length {profile.Data.Length}.");
|
||||
}
|
||||
else
|
||||
@@ -127,7 +136,6 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
// No datum (not generated yet, or not a Mix) — empty backdrop; the detail page still
|
||||
// renders its content over a plain background.
|
||||
_profile = null;
|
||||
_hasDatum = false;
|
||||
DebugLog(result.Success
|
||||
? $"datum fetch returned EMPTY/absent (no stored datum for ReleaseId={ReleaseId}) — backdrop stays blank."
|
||||
: $"datum fetch FAILED ({result.GetMessage() ?? "unknown error"}) — backdrop stays blank.");
|
||||
@@ -165,8 +173,10 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// Seed the module with the current state now that it exists.
|
||||
await PushZoomAsync();
|
||||
// Seed the module with the current state now that it exists. All four control values
|
||||
// come from the shared (session-persisted) state, so a mix opened mid-session seeds the
|
||||
// module with the slider positions the listener left them at.
|
||||
await PushControlsAsync();
|
||||
await PushDatumAsync();
|
||||
await PushPlaybackAsync();
|
||||
await PushThemeIfChangedAsync();
|
||||
@@ -177,16 +187,29 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
await PushThemeIfChangedAsync();
|
||||
}
|
||||
|
||||
private async Task OnZoomFractionChanged(double fraction)
|
||||
// The controls row mutated a slider on the shared state and raised Changed. Push all four control
|
||||
// uniforms (cheap scalar interop; the inert three are no-ops in the parity shader until Wave 3).
|
||||
private void OnControlStateChanged() => InvokeAsync(async () =>
|
||||
{
|
||||
ZoomState.VisibleSeconds = MixZoomMapping.FractionToSeconds(fraction);
|
||||
DebugLog($"zoom slider changed — raw fraction={fraction:F3} → visibleSeconds={ZoomState.VisibleSeconds:F3}s; pushing setZoom (handle={(_handle is null ? "null" : "ready")}).");
|
||||
await PushZoomAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
await PushControlsAsync();
|
||||
});
|
||||
|
||||
// ── Bridge pushes. Each is a no-op until the module handle exists. ───────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Push all four control values to the module from the shared state. Used to seed on first render
|
||||
/// and to re-push when the controls row signals a change. Resolution drives the live render; the
|
||||
/// other three are inert in the parity shader (Wave 3 consumes them).
|
||||
/// </summary>
|
||||
private async Task PushControlsAsync()
|
||||
{
|
||||
if (_handle is null) return;
|
||||
await _handle.InvokeVoidAsync("setZoom", ControlState.VisibleSeconds);
|
||||
await _handle.InvokeVoidAsync("setBubblyness", ControlState.Bubblyness);
|
||||
await _handle.InvokeVoidAsync("setDetach", ControlState.Detach);
|
||||
await _handle.InvokeVoidAsync("setColorShiftSpeed", ControlState.ColorShiftSpeed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Push the datum to the module, but only when it actually changed — a different profile, or the
|
||||
/// mix duration becoming available for the first time. Idempotent so the per-tick playback path
|
||||
@@ -242,12 +265,6 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
await _handle.InvokeVoidAsync("setPlayback", CurrentPositionSeconds, IsPlaying);
|
||||
}
|
||||
|
||||
private async Task PushZoomAsync()
|
||||
{
|
||||
if (_handle is null) return;
|
||||
await _handle.InvokeVoidAsync("setZoom", ZoomState.VisibleSeconds);
|
||||
}
|
||||
|
||||
private async Task PushThemeIfChangedAsync()
|
||||
{
|
||||
if (_handle is null) return;
|
||||
@@ -297,6 +314,12 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable
|
||||
_subscribedService = null;
|
||||
}
|
||||
|
||||
if (_subscribedToControls)
|
||||
{
|
||||
ControlState.Changed -= OnControlStateChanged;
|
||||
_subscribedToControls = false;
|
||||
}
|
||||
|
||||
if (_handle is not null)
|
||||
{
|
||||
try { await _handle.InvokeVoidAsync("dispose"); } catch (JSDisconnectedException) { }
|
||||
|
||||
Reference in New Issue
Block a user