Add split capture to coach dashboard timer

This commit is contained in:
axiomlogicnexus 2026-04-27 14:26:22 +02:00
parent 1d72a8e7a5
commit 1251369cb7
2 changed files with 258 additions and 0 deletions

View file

@ -134,6 +134,7 @@ void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView()
LiveAttemptInspectionElapsedMs = 0;
LiveAttemptSolveElapsedMs = 0;
LiveAttemptDisplayedMs = 0;
LiveAttemptSplitCaptures.Reset();
}
else
{
@ -320,6 +321,7 @@ bool UHyperTwistCoachDashboardWidget::StartLiveAttemptTimer()
LiveAttemptInspectionElapsedMs = 0;
LiveAttemptSolveElapsedMs = 0;
LiveAttemptDisplayedMs = 0;
LiveAttemptSplitCaptures.Reset();
UpdateDashboardPresentation();
return true;
}
@ -349,6 +351,7 @@ void UHyperTwistCoachDashboardWidget::CancelLiveAttemptTimer()
LiveAttemptInspectionElapsedMs = 0;
LiveAttemptSolveElapsedMs = 0;
LiveAttemptDisplayedMs = 0;
LiveAttemptSplitCaptures.Reset();
UpdateDashboardPresentation();
}
@ -380,6 +383,7 @@ FHyperTwistTrainingRunStepResult UHyperTwistCoachDashboardWidget::SubmitLiveTime
Attempt.TimingBreakdown.InspectionElapsedMs = FMath::Max(LiveAttemptInspectionElapsedMs, 0);
Attempt.TimingBreakdown.RawSolveTimeMs = FMath::Max(LiveAttemptSolveElapsedMs, 0);
Attempt.TimingBreakdown.FinalTimeMs = FMath::Max(LiveAttemptSolveElapsedMs, 0);
Attempt.TimingBreakdown.SplitCaptures = LiveAttemptSplitCaptures;
Attempt.TimingBreakdown.bDidNotFinish =
Result == EHyperTwistTrainingAttemptResult::Timeout
|| Result == EHyperTwistTrainingAttemptResult::DNF
@ -400,6 +404,43 @@ FHyperTwistTrainingRunStepResult UHyperTwistCoachDashboardWidget::SubmitLiveTime
return StepResult;
}
bool UHyperTwistCoachDashboardWidget::CaptureNextLiveAttemptSplit()
{
if (!bHasLiveAttemptTimer || LiveAttemptPhase != EHyperTwistCoachDashboardAttemptPhase::Solving)
{
return false;
}
const FHyperTwistTrainingSplitPhaseDefinition* NextPhaseDefinition = FindNextSplitPhaseToCapture();
if (NextPhaseDefinition == nullptr)
{
return false;
}
RefreshLiveAttemptClock();
FHyperTwistTrainingSplitCapture SplitCapture;
SplitCapture.PhaseId = NextPhaseDefinition->PhaseId;
SplitCapture.Label = NextPhaseDefinition->Label;
SplitCapture.TimestampMs = FMath::Max(LiveAttemptSolveElapsedMs, 0);
LiveAttemptSplitCaptures.Add(SplitCapture);
UpdateDashboardPresentation();
return true;
}
bool UHyperTwistCoachDashboardWidget::UndoLastLiveAttemptSplit()
{
if (LiveAttemptSplitCaptures.Num() <= 0)
{
return false;
}
LiveAttemptSplitCaptures.RemoveAt(LiveAttemptSplitCaptures.Num() - 1);
UpdateDashboardPresentation();
return true;
}
void UHyperTwistCoachDashboardWidget::HandlePrimaryActionClicked()
{
ExecutePrimaryCoachAction();
@ -553,6 +594,16 @@ void UHyperTwistCoachDashboardWidget::HandleCancelTimerClicked()
CancelLiveAttemptTimer();
}
void UHyperTwistCoachDashboardWidget::HandleCaptureSplitClicked()
{
CaptureNextLiveAttemptSplit();
}
void UHyperTwistCoachDashboardWidget::HandleUndoSplitClicked()
{
UndoLastLiveAttemptSplit();
}
void UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked()
{
SelectPreviousQueueEntry();
@ -665,6 +716,18 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
WidgetTree,
RootLayout,
TEXT("CoachTimerStatus"),
2
);
SplitHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachSplitHeader"),
2
);
SplitStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachSplitStatus"),
8
);
FollowUpTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
@ -874,6 +937,40 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
CancelTimerButtonLabel = CancelTimerLabelRaw;
}
UHorizontalBox* SplitCaptureRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("CoachDashboardSplitCaptureRow")
);
if (SplitCaptureRow != nullptr)
{
if (UVerticalBoxSlot* SplitCaptureRowSlot = RootLayout->AddChildToVerticalBox(SplitCaptureRow))
{
SplitCaptureRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f));
}
UTextBlock* CaptureSplitLabelRaw = nullptr;
CaptureSplitButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
SplitCaptureRow,
TEXT("CaptureSplitButton"),
TEXT("CaptureSplitButtonLabel"),
TEXT("Capture Split"),
CaptureSplitLabelRaw
);
CaptureSplitButtonLabel = CaptureSplitLabelRaw;
UTextBlock* UndoSplitLabelRaw = nullptr;
UndoSplitButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
SplitCaptureRow,
TEXT("UndoSplitButton"),
TEXT("UndoSplitButtonLabel"),
TEXT("Undo Split"),
UndoSplitLabelRaw
);
UndoSplitButtonLabel = UndoSplitLabelRaw;
}
UHorizontalBox* RunControlRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("CoachDashboardRunControlButtons")
@ -1053,6 +1150,14 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
{
CancelTimerButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleCancelTimerClicked);
}
if (CaptureSplitButton != nullptr)
{
CaptureSplitButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleCaptureSplitClicked);
}
if (UndoSplitButton != nullptr)
{
UndoSplitButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleUndoSplitClicked);
}
if (PreviousQueueButton != nullptr)
{
PreviousQueueButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked);
@ -1147,6 +1252,32 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
const bool bLiveInspectionPhase = IsLiveAttemptInspectionPhase();
const bool bLiveSolvePhase = bHasLiveAttemptTimer
&& LiveAttemptPhase == EHyperTwistCoachDashboardAttemptPhase::Solving;
const int32 MaxManualSplitCaptureCount = GetMaxManualSplitCaptureCount();
const FHyperTwistTrainingSplitPhaseDefinition* NextSplitPhaseDefinition = FindNextSplitPhaseToCapture();
const FString NextSplitLabel = NextSplitPhaseDefinition == nullptr
? TEXT("none")
: (NextSplitPhaseDefinition->Label.IsEmpty()
? NextSplitPhaseDefinition->PhaseId
: NextSplitPhaseDefinition->Label);
FString CapturedSplitSummary = TEXT("none");
if (LiveAttemptSplitCaptures.Num() > 0)
{
TArray<FString> CapturedSplitParts;
CapturedSplitParts.Reserve(LiveAttemptSplitCaptures.Num());
for (const FHyperTwistTrainingSplitCapture& SplitCapture : LiveAttemptSplitCaptures)
{
const FString Label = SplitCapture.Label.IsEmpty() ? SplitCapture.PhaseId : SplitCapture.Label;
CapturedSplitParts.Add(FString::Printf(TEXT("%s@%d"), *Label, SplitCapture.TimestampMs));
}
CapturedSplitSummary = FString::Join(CapturedSplitParts, TEXT(" | "));
}
const FString FinalSplitLabel =
TimingPolicy != nullptr
&& TimingPolicy->SplitPhases.IsValidIndex(MaxManualSplitCaptureCount)
? (TimingPolicy->SplitPhases[MaxManualSplitCaptureCount].Label.IsEmpty()
? TimingPolicy->SplitPhases[MaxManualSplitCaptureCount].PhaseId
: TimingPolicy->SplitPhases[MaxManualSplitCaptureCount].Label)
: TEXT("solve end");
const FString RunningSolveLabel =
TimingPolicy != nullptr && TimingPolicy->bHideRunningTime && bLiveSolvePhase
? TEXT("hidden")
@ -1337,6 +1468,48 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
)));
}
}
if (SplitHeaderTextBlock != nullptr)
{
SplitHeaderTextBlock->SetText(FText::FromString(TEXT("[Split Capture]")));
}
if (SplitStatusTextBlock != nullptr)
{
if (MaxManualSplitCaptureCount <= 0)
{
SplitStatusTextBlock->SetText(FText::FromString(
TEXT("Split capture: no manual split phases are configured for this timing policy.")
));
}
else
{
const FString SplitStateLine = FString::Printf(
TEXT("Manual splits %d/%d | Next: %s | Final auto-close: %s | Captured: %s"),
LiveAttemptSplitCaptures.Num(),
MaxManualSplitCaptureCount,
*NextSplitLabel,
*FinalSplitLabel,
*CapturedSplitSummary
);
if (bLiveSolvePhase)
{
SplitStatusTextBlock->SetText(FText::FromString(
SplitStateLine + TEXT(" | capture phase ends during solve; final phase closes on attempt stop")
));
}
else if (bLiveInspectionPhase)
{
SplitStatusTextBlock->SetText(FText::FromString(
SplitStateLine + TEXT(" | split capture unlocks after solve start")
));
}
else
{
SplitStatusTextBlock->SetText(FText::FromString(
SplitStateLine + TEXT(" | start a live attempt to record manual split boundaries")
));
}
}
}
if (AttemptTimeTextBox != nullptr)
{
const FString HintLabel = CurrentTrainingCase.TimeTargetMs > 0
@ -1659,6 +1832,25 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
CancelTimerButton->SetIsEnabled(bHasLiveAttemptTimer);
}
if (CaptureSplitButtonLabel != nullptr)
{
const FString CaptureSplitLabel = NextSplitPhaseDefinition == nullptr
? TEXT("All Splits Captured")
: FString::Printf(TEXT("Capture %s"), *NextSplitLabel);
CaptureSplitButtonLabel->SetText(FText::FromString(CaptureSplitLabel));
}
if (CaptureSplitButton != nullptr)
{
CaptureSplitButton->SetIsEnabled(bLiveSolvePhase && NextSplitPhaseDefinition != nullptr);
}
if (UndoSplitButtonLabel != nullptr)
{
UndoSplitButtonLabel->SetText(FText::FromString(TEXT("Undo Split")));
}
if (UndoSplitButton != nullptr)
{
UndoSplitButton->SetIsEnabled(bLiveSolvePhase && LiveAttemptSplitCaptures.Num() > 0);
}
if (PreviousQueueButtonLabel != nullptr)
{
PreviousQueueButtonLabel->SetText(FText::FromString(TEXT("Prev Queue")));
@ -1851,3 +2043,34 @@ bool UHyperTwistCoachDashboardWidget::IsLiveAttemptInspectionPhase() const
{
return bHasLiveAttemptTimer && LiveAttemptPhase == EHyperTwistCoachDashboardAttemptPhase::Inspection;
}
int32 UHyperTwistCoachDashboardWidget::GetMaxManualSplitCaptureCount() const
{
const FHyperTwistTrainingTimingPolicy* TimingPolicy = FindActiveTimingPolicy();
if (TimingPolicy == nullptr || TimingPolicy->SplitPhases.Num() <= 1)
{
return 0;
}
return TimingPolicy->SplitPhases.Num() - 1;
}
const FHyperTwistTrainingSplitPhaseDefinition* UHyperTwistCoachDashboardWidget::FindNextSplitPhaseToCapture() const
{
const FHyperTwistTrainingTimingPolicy* TimingPolicy = FindActiveTimingPolicy();
if (TimingPolicy == nullptr)
{
return nullptr;
}
const int32 NextPhaseIndex = LiveAttemptSplitCaptures.Num();
const int32 MaxManualSplitCaptureCount = GetMaxManualSplitCaptureCount();
if (NextPhaseIndex < 0
|| NextPhaseIndex >= MaxManualSplitCaptureCount
|| !TimingPolicy->SplitPhases.IsValidIndex(NextPhaseIndex))
{
return nullptr;
}
return &TimingPolicy->SplitPhases[NextPhaseIndex];
}

View file

@ -61,6 +61,9 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
int32 LiveAttemptDisplayedMs = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
TArray<FHyperTwistTrainingSplitCapture> LiveAttemptSplitCaptures;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
void RefreshCoachDashboardView();
@ -102,6 +105,12 @@ public:
EHyperTwistTrainingAttemptResult Result
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool CaptureNextLiveAttemptSplit();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool UndoLastLiveAttemptSplit();
protected:
UPROPERTY(Transient)
TObjectPtr<UVerticalBox> RootLayout = nullptr;
@ -148,6 +157,12 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TimerStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> SplitHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> SplitStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UEditableTextBox> AttemptTimeTextBox = nullptr;
@ -271,6 +286,18 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CancelTimerButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> CaptureSplitButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CaptureSplitButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> UndoSplitButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> UndoSplitButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousQueueButton = nullptr;
@ -331,6 +358,12 @@ protected:
UFUNCTION()
void HandleCancelTimerClicked();
UFUNCTION()
void HandleCaptureSplitClicked();
UFUNCTION()
void HandleUndoSplitClicked();
UFUNCTION()
void HandlePreviousQueueClicked();
@ -353,6 +386,8 @@ private:
const FHyperTwistTrainingCoachSessionQueueStateEntry* FindSelectedQueueEntry() const;
const FHyperTwistTrainingTimingPolicy* FindActiveTimingPolicy() const;
bool IsLiveAttemptInspectionPhase() const;
int32 GetMaxManualSplitCaptureCount() const;
const FHyperTwistTrainingSplitPhaseDefinition* FindNextSplitPhaseToCapture() const;
EHyperTwistCoachDashboardAttemptPhase LiveAttemptPhase = EHyperTwistCoachDashboardAttemptPhase::Idle;
double LiveAttemptStartedAtSeconds = 0.0;