Add twisty Bound 4 visualization packet

This commit is contained in:
axiomlogicnexus 2026-05-18 06:30:28 +02:00
parent dc621d97e4
commit 1f20628b7a
4 changed files with 721 additions and 1 deletions

View file

@ -308,6 +308,104 @@ namespace HyperTwistPuzzleViewerComponentInternal
: 0;
}
EHyperTwistViewerVisualizationMode ResolvePreferredVisualizationMode(
const EHyperTwistViewerVisualizationPreference Preference
)
{
return Preference == EHyperTwistViewerVisualizationPreference::Prefer3D
? EHyperTwistViewerVisualizationMode::ThreeD
: EHyperTwistViewerVisualizationMode::TwoD;
}
EHyperTwistViewerPresentationMode ResolvePresentationMode(
const EHyperTwistViewerPresentationMode RequestedMode,
const bool bAllowExpandedView,
const bool bAllowFullscreen
)
{
switch (RequestedMode)
{
case EHyperTwistViewerPresentationMode::Fullscreen:
if (bAllowFullscreen)
{
return EHyperTwistViewerPresentationMode::Fullscreen;
}
if (bAllowExpandedView)
{
return EHyperTwistViewerPresentationMode::Expanded;
}
return EHyperTwistViewerPresentationMode::Inline;
case EHyperTwistViewerPresentationMode::Expanded:
return bAllowExpandedView
? EHyperTwistViewerPresentationMode::Expanded
: EHyperTwistViewerPresentationMode::Inline;
default:
return EHyperTwistViewerPresentationMode::Inline;
}
}
bool SupportsThreeDVisualization(
const FHyperTwistViewerEmbeddedHostBootstrapState& BootstrapState,
const FHyperTwistTrainingViewerEmbedContract& EmbedContract,
const FHyperTwistTrainingSpatialUiContract& UiContract,
const FHyperTwistTrainingSpatialEffectBoundary& EffectBoundary,
const FHyperTwistTrainingSpatialStateMotionContract& StateMotionContract
)
{
return BootstrapState.bReadyForEmbed
&& BootstrapState.BindingMode == EHyperTwistViewerEmbeddedHostBindingMode::ScissoredView
&& BootstrapState.RenderStateProfile == TEXT("classic-replay")
&& BootstrapState.SpatialEventSurfaceTags.Contains(TEXT("scissored view embedding"))
&& BootstrapState.SpatialEventSurfaceTags.Contains(TEXT("HTML overlay bridge"))
&& EmbedContract.ShippableBehaviorTags.Contains(TEXT("orbit-controls"))
&& UiContract.StateSurfaceTags.Contains(TEXT("HTML/spatial overlay bridge"))
&& EffectBoundary.EffectFamilies.Contains(TEXT("tone mapping and highlight effects"))
&& StateMotionContract.MotionKinds.Contains(TEXT("reduced-motion support"));
}
EHyperTwistViewerVisualizationFallbackReason ResolveThreeDFallbackReason(
const FHyperTwistViewerEmbeddedHostBootstrapState& BootstrapState,
const FHyperTwistTrainingViewerEmbedContract& EmbedContract,
const FHyperTwistTrainingSpatialUiContract& UiContract,
const FHyperTwistTrainingSpatialEffectBoundary& EffectBoundary,
const FHyperTwistTrainingSpatialStateMotionContract& StateMotionContract
)
{
if (BootstrapState.BindingMode != EHyperTwistViewerEmbeddedHostBindingMode::ScissoredView)
{
return EHyperTwistViewerVisualizationFallbackReason::HostBindingDoesNotSupport3D;
}
if (BootstrapState.RenderStateProfile != TEXT("classic-replay"))
{
return EHyperTwistViewerVisualizationFallbackReason::RenderProfilePrefers2D;
}
return SupportsThreeDVisualization(BootstrapState, EmbedContract, UiContract, EffectBoundary, StateMotionContract)
? EHyperTwistViewerVisualizationFallbackReason::None
: EHyperTwistViewerVisualizationFallbackReason::MissingSupportContract;
}
bool CanApplyPresentationMode(
const FHyperTwistViewerVisualizationShellState& VisualizationState,
const EHyperTwistViewerPresentationMode NewMode
)
{
switch (NewMode)
{
case EHyperTwistViewerPresentationMode::Fullscreen:
return VisualizationState.bSupportsFullscreen;
case EHyperTwistViewerPresentationMode::Expanded:
return VisualizationState.bSupportsExpandedView;
default:
return true;
}
}
FHyperTwistViewerPositionSnapshot BuildPositionSnapshot(
const TArray<FHyperTwistViewerMoveEntry>& MoveList,
const TArray<int32>& MoveBoundaryTimesMs,
@ -487,6 +585,7 @@ void UHyperTwistPuzzleViewerComponent::LoadSceneContext(const FHyperTwistSimulat
SceneContext = InContext;
PlaybackState.bSceneContextLoaded = InContext.IsStructurallyValid();
HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState();
VisualizationShellState = FHyperTwistViewerVisualizationShellState();
// Reset playhead — loaded content has changed.
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stopped;
@ -507,6 +606,7 @@ void UHyperTwistPuzzleViewerComponent::LoadMoveList(const TArray<FHyperTwistView
ReplaySummary = FHyperTwistDerivedReplaySummary();
bReplayPacketLoaded = false;
HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState();
VisualizationShellState = FHyperTwistViewerVisualizationShellState();
PlaybackState.Mode = EHyperTwistViewerPlaybackMode::Stopped;
PlaybackState.Direction = EHyperTwistViewerPlaybackDirection::None;
@ -531,6 +631,7 @@ bool UHyperTwistPuzzleViewerComponent::LoadReplayPacket(const FHyperTwistReplayP
bReplayPacketLoaded = true;
MoveList = HyperTwistPuzzleViewerComponentInternal::BuildReplayMoveList(InReplayPacket);
HostBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState();
VisualizationShellState = FHyperTwistViewerVisualizationShellState();
FHyperTwistSimulationSceneContext ReplaySceneContext;
if (HyperTwistPuzzleViewerComponentInternal::TryBuildSceneContextFromReplay(InReplayPacket, ReplaySceneContext))
@ -808,6 +909,7 @@ bool UHyperTwistPuzzleViewerComponent::TryBootstrapEmbeddedClassicCubingHost(
)
{
OutBootstrapState = FHyperTwistViewerEmbeddedHostBootstrapState();
VisualizationShellState = FHyperTwistViewerVisualizationShellState();
if (!Request.IsStructurallyValid())
{
@ -876,6 +978,170 @@ bool UHyperTwistPuzzleViewerComponent::TryBootstrapEmbeddedClassicCubingHost(
return true;
}
bool UHyperTwistPuzzleViewerComponent::TryBuildVisualizationShell(
const FHyperTwistViewerVisualizationShellRequest& Request,
FHyperTwistViewerVisualizationShellState& OutVisualizationShellState
)
{
OutVisualizationShellState = FHyperTwistViewerVisualizationShellState();
if (!Request.IsStructurallyValid())
{
VisualizationShellState = FHyperTwistViewerVisualizationShellState();
return false;
}
FHyperTwistViewerEmbeddedHostBootstrapState BootstrapState;
if (!TryBootstrapEmbeddedClassicCubingHost(Request.HostBootstrapRequest, BootstrapState))
{
VisualizationShellState = FHyperTwistViewerVisualizationShellState();
return false;
}
const FHyperTwistTrainingViewerReferenceBundle BrowserViewerBundle =
UHyperTwistTrainingViewerLibrary::BuildBundledBrowserViewerReferenceBundle();
const FHyperTwistTrainingSpatialReferenceBundle SpatialBundle =
UHyperTwistTrainingSpatialLibrary::BuildBundledBrowserSpatialReferenceBundle();
FHyperTwistTrainingViewerEmbedContract EmbedContract;
FHyperTwistTrainingSpatialUiContract UiContract;
FHyperTwistTrainingSpatialEffectBoundary EffectBoundary;
FHyperTwistTrainingSpatialStateMotionContract StateMotionContract;
if (!UHyperTwistTrainingViewerLibrary::TryGetEmbedContractById(
BrowserViewerBundle,
BrowserViewerBundle.PrimaryEmbedContractId,
EmbedContract
)
|| !UHyperTwistTrainingSpatialLibrary::TryGetSpatialUiContractById(
SpatialBundle,
SpatialBundle.PrimarySpatialUiContractId,
UiContract
)
|| !UHyperTwistTrainingSpatialLibrary::TryGetEffectBoundaryById(
SpatialBundle,
SpatialBundle.PrimaryEffectBoundaryId,
EffectBoundary
)
|| !UHyperTwistTrainingSpatialLibrary::TryGetStateMotionContractById(
SpatialBundle,
SpatialBundle.PrimaryStateMotionContractId,
StateMotionContract
))
{
VisualizationShellState = FHyperTwistViewerVisualizationShellState();
return false;
}
VisualizationShellState = FHyperTwistViewerVisualizationShellState();
VisualizationShellState.VisualizationId = FString::Printf(
TEXT("visualization/%s/%s"),
*Request.HostBootstrapRequest.HostInstanceId,
*BootstrapState.SceneContextId
);
VisualizationShellState.BootstrapId = BootstrapState.BootstrapId;
VisualizationShellState.AdapterProjectionId = BootstrapState.AdapterProjectionId;
VisualizationShellState.ViewerAdapterContractId = BootstrapState.ViewerAdapterContractId;
VisualizationShellState.BrowserEmbedContractId = EmbedContract.ContractId;
VisualizationShellState.SpatialSceneContractId = BootstrapState.SpatialSceneContractId;
VisualizationShellState.SpatialUiContractId = UiContract.ContractId;
VisualizationShellState.SpatialEffectBoundaryId = EffectBoundary.BoundaryId;
VisualizationShellState.SpatialStateMotionContractId = StateMotionContract.ContractId;
VisualizationShellState.SceneContextId = BootstrapState.SceneContextId;
VisualizationShellState.RenderStateProfile = BootstrapState.RenderStateProfile;
VisualizationShellState.PreferredMode =
HyperTwistPuzzleViewerComponentInternal::ResolvePreferredVisualizationMode(Request.VisualizationPreference);
if (VisualizationShellState.PreferredMode == EHyperTwistViewerVisualizationMode::ThreeD)
{
const bool bSupportsThreeD = HyperTwistPuzzleViewerComponentInternal::SupportsThreeDVisualization(
BootstrapState,
EmbedContract,
UiContract,
EffectBoundary,
StateMotionContract
);
if (bSupportsThreeD)
{
VisualizationShellState.ActiveMode = EHyperTwistViewerVisualizationMode::ThreeD;
VisualizationShellState.bUsingFallback = false;
VisualizationShellState.FallbackReason = EHyperTwistViewerVisualizationFallbackReason::None;
VisualizationShellState.bUsesSpatialRendering = true;
}
else if (Request.bAllow2DFallback)
{
VisualizationShellState.ActiveMode = EHyperTwistViewerVisualizationMode::TwoD;
VisualizationShellState.bUsingFallback = true;
VisualizationShellState.FallbackReason =
HyperTwistPuzzleViewerComponentInternal::ResolveThreeDFallbackReason(
BootstrapState,
EmbedContract,
UiContract,
EffectBoundary,
StateMotionContract
);
VisualizationShellState.bUsesSpatialRendering = false;
}
else
{
VisualizationShellState = FHyperTwistViewerVisualizationShellState();
return false;
}
}
else
{
VisualizationShellState.ActiveMode = EHyperTwistViewerVisualizationMode::TwoD;
VisualizationShellState.bUsingFallback = false;
VisualizationShellState.FallbackReason = EHyperTwistViewerVisualizationFallbackReason::None;
VisualizationShellState.bUsesSpatialRendering = false;
}
VisualizationShellState.PresentationMode = HyperTwistPuzzleViewerComponentInternal::ResolvePresentationMode(
Request.InitialPresentationMode,
Request.bAllowExpandedView,
Request.bAllowFullscreen
);
VisualizationShellState.bReadyForVisualization = true;
VisualizationShellState.bSupportsExpandedView = Request.bAllowExpandedView;
VisualizationShellState.bSupportsFullscreen = Request.bAllowFullscreen;
VisualizationShellState.ViewerBehaviorTags = EmbedContract.ShippableBehaviorTags;
VisualizationShellState.ViewerAnnotationSurfaceTags = EmbedContract.AnnotationSurfaceTags;
VisualizationShellState.SpatialUiComponentFamilies = UiContract.ComponentFamilies;
VisualizationShellState.SpatialUiStateSurfaceTags = UiContract.StateSurfaceTags;
VisualizationShellState.SpatialSceneEventSurfaceTags = BootstrapState.SpatialEventSurfaceTags;
VisualizationShellState.SpatialEffectFamilies = EffectBoundary.EffectFamilies;
VisualizationShellState.MotionKinds = StateMotionContract.MotionKinds;
if (!VisualizationShellState.IsStructurallyValid())
{
VisualizationShellState = FHyperTwistViewerVisualizationShellState();
return false;
}
OutVisualizationShellState = VisualizationShellState;
return true;
}
bool UHyperTwistPuzzleViewerComponent::TrySetVisualizationPresentationMode(
const EHyperTwistViewerPresentationMode NewMode,
FHyperTwistViewerVisualizationShellState& OutVisualizationShellState
)
{
OutVisualizationShellState = FHyperTwistViewerVisualizationShellState();
if (!VisualizationShellState.IsStructurallyValid()
|| !VisualizationShellState.bReadyForVisualization
|| !HyperTwistPuzzleViewerComponentInternal::CanApplyPresentationMode(VisualizationShellState, NewMode))
{
return false;
}
VisualizationShellState.PresentationMode = NewMode;
OutVisualizationShellState = VisualizationShellState;
return true;
}
// ---------------------------------------------------------------------------
// Readout
// ---------------------------------------------------------------------------

View file

@ -60,7 +60,7 @@ DECLARE_MULTICAST_DELEGATE_OneParam(
//
// Bound 2 adds observer channels and deterministic cursor/timeline transport.
// Bound 3 adds adapter projection and embedded-host bootstrap surfaces.
// Viewport rendering is Bound 4.
// Bound 4 adds a visualization shell with local fallback and presentation modes.
// ---------------------------------------------------------------------------
UCLASS(ClassGroup = "HyperTwist", meta = (BlueprintSpawnableComponent), DisplayName = "HyperTwist Puzzle Viewer")
@ -121,6 +121,23 @@ public:
FHyperTwistViewerEmbeddedHostBootstrapState& OutBootstrapState
);
// Build a bounded visualization shell on top of the closed replay, adapter,
// and browser-support layers without taking browser or classic-cubing
// ownership into this lane.
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Viewer")
bool TryBuildVisualizationShell(
const FHyperTwistViewerVisualizationShellRequest& Request,
FHyperTwistViewerVisualizationShellState& OutVisualizationShellState
);
// Update only the visualization-local presentation mode for the current
// shell without widening into host-lifecycle ownership.
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Viewer")
bool TrySetVisualizationPresentationMode(
EHyperTwistViewerPresentationMode NewMode,
FHyperTwistViewerVisualizationShellState& OutVisualizationShellState
);
// ------------------------------------------------------------------
// Readout accessors
// ------------------------------------------------------------------
@ -152,6 +169,9 @@ public:
UFUNCTION(BlueprintPure, Category = "HyperTwist|Viewer")
FHyperTwistViewerEmbeddedHostBootstrapState GetHostBootstrapState() const { return HostBootstrapState; }
UFUNCTION(BlueprintPure, Category = "HyperTwist|Viewer")
FHyperTwistViewerVisualizationShellState GetVisualizationShellState() const { return VisualizationShellState; }
// ------------------------------------------------------------------
// Delegates
// ------------------------------------------------------------------
@ -205,6 +225,9 @@ private:
UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer")
FHyperTwistViewerEmbeddedHostBootstrapState HostBootstrapState;
UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer")
FHyperTwistViewerVisualizationShellState VisualizationShellState;
UPROPERTY(VisibleAnywhere, Category = "HyperTwist|Viewer")
TArray<int32> MoveBoundaryTimesMs;

View file

@ -49,6 +49,37 @@ enum class EHyperTwistViewerEmbeddedHostBindingMode : uint8
ScissoredView UMETA(DisplayName = "Scissored View")
};
UENUM(BlueprintType)
enum class EHyperTwistViewerVisualizationMode : uint8
{
TwoD UMETA(DisplayName = "2D"),
ThreeD UMETA(DisplayName = "3D")
};
UENUM(BlueprintType)
enum class EHyperTwistViewerVisualizationPreference : uint8
{
Prefer2D UMETA(DisplayName = "Prefer 2D"),
Prefer3D UMETA(DisplayName = "Prefer 3D")
};
UENUM(BlueprintType)
enum class EHyperTwistViewerVisualizationFallbackReason : uint8
{
None UMETA(DisplayName = "None"),
HostBindingDoesNotSupport3D UMETA(DisplayName = "Host Binding Does Not Support 3D"),
RenderProfilePrefers2D UMETA(DisplayName = "Render Profile Prefers 2D"),
MissingSupportContract UMETA(DisplayName = "Missing Support Contract")
};
UENUM(BlueprintType)
enum class EHyperTwistViewerPresentationMode : uint8
{
Inline UMETA(DisplayName = "Inline"),
Expanded UMETA(DisplayName = "Expanded"),
Fullscreen UMETA(DisplayName = "Fullscreen")
};
// ---------------------------------------------------------------------------
// Control action — what the caller wants the viewer to do
// ---------------------------------------------------------------------------
@ -571,6 +602,165 @@ struct UNREALHYPERTWIST_API FHyperTwistViewerEmbeddedHostBootstrapState
}
};
// ---------------------------------------------------------------------------
// Bound 4 visualization-shell records
// ---------------------------------------------------------------------------
USTRUCT(BlueprintType)
struct UNREALHYPERTWIST_API FHyperTwistViewerVisualizationShellRequest
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FHyperTwistViewerEmbeddedHostBootstrapRequest HostBootstrapRequest;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
EHyperTwistViewerVisualizationPreference VisualizationPreference =
EHyperTwistViewerVisualizationPreference::Prefer3D;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
EHyperTwistViewerPresentationMode InitialPresentationMode = EHyperTwistViewerPresentationMode::Inline;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
bool bAllow2DFallback = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
bool bAllowExpandedView = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
bool bAllowFullscreen = true;
bool IsStructurallyValid() const
{
return HostBootstrapRequest.IsStructurallyValid();
}
};
USTRUCT(BlueprintType)
struct UNREALHYPERTWIST_API FHyperTwistViewerVisualizationShellState
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FString VisualizationId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FString BootstrapId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FString AdapterProjectionId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FString ViewerAdapterContractId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FString BrowserEmbedContractId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FString SpatialSceneContractId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FString SpatialUiContractId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FString SpatialEffectBoundaryId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FString SpatialStateMotionContractId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FString SceneContextId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
FString RenderStateProfile;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
EHyperTwistViewerVisualizationMode PreferredMode = EHyperTwistViewerVisualizationMode::ThreeD;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
EHyperTwistViewerVisualizationMode ActiveMode = EHyperTwistViewerVisualizationMode::TwoD;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
EHyperTwistViewerVisualizationFallbackReason FallbackReason =
EHyperTwistViewerVisualizationFallbackReason::None;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
EHyperTwistViewerPresentationMode PresentationMode = EHyperTwistViewerPresentationMode::Inline;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
bool bReadyForVisualization = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
bool bUsingFallback = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
bool bSupportsExpandedView = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
bool bSupportsFullscreen = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
bool bUsesSpatialRendering = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> ViewerBehaviorTags;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> ViewerAnnotationSurfaceTags;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> SpatialUiComponentFamilies;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> SpatialUiStateSurfaceTags;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> SpatialSceneEventSurfaceTags;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> SpatialEffectFamilies;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Viewer")
TArray<FString> MotionKinds;
bool IsStructurallyValid() const
{
if (!bReadyForVisualization)
{
return VisualizationId.IsEmpty()
&& BootstrapId.IsEmpty()
&& AdapterProjectionId.IsEmpty()
&& ViewerAdapterContractId.IsEmpty()
&& BrowserEmbedContractId.IsEmpty()
&& SpatialSceneContractId.IsEmpty()
&& SpatialUiContractId.IsEmpty()
&& SpatialEffectBoundaryId.IsEmpty()
&& SpatialStateMotionContractId.IsEmpty()
&& SceneContextId.IsEmpty()
&& RenderStateProfile.IsEmpty();
}
return !VisualizationId.IsEmpty()
&& !BootstrapId.IsEmpty()
&& !AdapterProjectionId.IsEmpty()
&& !ViewerAdapterContractId.IsEmpty()
&& !BrowserEmbedContractId.IsEmpty()
&& !SpatialSceneContractId.IsEmpty()
&& !SpatialUiContractId.IsEmpty()
&& !SpatialEffectBoundaryId.IsEmpty()
&& !SpatialStateMotionContractId.IsEmpty()
&& !SceneContextId.IsEmpty()
&& !RenderStateProfile.IsEmpty()
&& ViewerBehaviorTags.Num() > 0
&& ViewerAnnotationSurfaceTags.Num() > 0
&& SpatialUiComponentFamilies.Num() > 0
&& SpatialUiStateSurfaceTags.Num() > 0
&& SpatialSceneEventSurfaceTags.Num() > 0
&& SpatialEffectFamilies.Num() > 0
&& MotionKinds.Num() > 0;
}
};
// ---------------------------------------------------------------------------
// Runtime surface - minimal compact replay-player bundle exposed through the
// training runtime library without moving shell ownership out of simulation.

View file

@ -0,0 +1,241 @@
// Copyright HyperTwist, Inc. All Rights Reserved.
#include "Misc/AutomationTest.h"
#include "HyperTwistBootstrap/HyperTwistContractLibrary.h"
#include "HyperTwistSimulation/HyperTwistPuzzleViewerComponent.h"
#include "JsonObjectConverter.h"
#if WITH_AUTOMATION_TESTS
namespace HyperTwistTwistyBound4VisualizationTestInternal
{
template <typename TStruct>
FString SerializeStructToJson(const TStruct& Value)
{
FString Json;
FJsonObjectConverter::UStructToJsonObjectString(TStruct::StaticStruct(), &Value, Json, 0, 0);
return Json;
}
FHyperTwistReplayEvent MakeReplayEvent(
const FString& EventId,
const int32 Sequence,
const int32 TimeMs,
const EHyperTwistReplayEventType EventType,
const FString& PayloadJson
)
{
FHyperTwistReplayEvent Event;
Event.EventId = EventId;
Event.Sequence = Sequence;
Event.TimeMs = TimeMs;
Event.EventType = EventType;
Event.PayloadJson = PayloadJson;
return Event;
}
FHyperTwistReplayPacket MakeReplayPacket()
{
const FHyperTwistPuzzleState SampleState = UHyperTwistContractLibrary::MakeSampleClassicPuzzleState();
FHyperTwistStateSnapshot Snapshot;
Snapshot.SnapshotId = TEXT("snapshot-bound4");
Snapshot.State = SampleState;
Snapshot.DerivedHash = TEXT("snapshot-bound4-hash");
FHyperTwistReplayMovePayload FirstMovePayload;
FirstMovePayload.Notation = TEXT("R");
FirstMovePayload.TransformationRef = TEXT("classic/r");
FirstMovePayload.Source = TEXT("unit-test");
FHyperTwistReplayMovePayload SecondMovePayload;
SecondMovePayload.Notation = TEXT("U");
SecondMovePayload.TransformationRef = TEXT("classic/u");
SecondMovePayload.Source = TEXT("unit-test");
FHyperTwistReplayMovePayload ThirdMovePayload;
ThirdMovePayload.Notation = TEXT("F'");
ThirdMovePayload.TransformationRef = TEXT("classic/f-prime");
ThirdMovePayload.Source = TEXT("unit-test");
FHyperTwistReplayPacket ReplayPacket;
ReplayPacket.ReplayId = TEXT("replay-bound4-001");
ReplayPacket.SessionId = TEXT("session-bound4-001");
ReplayPacket.PuzzleDefinition = SampleState.Definition;
ReplayPacket.CaptureMode = EHyperTwistCaptureMode::Imported;
ReplayPacket.Events = {
MakeReplayEvent(
TEXT("evt-001"),
1,
0,
EHyperTwistReplayEventType::StateSnapshot,
SerializeStructToJson(Snapshot)
),
MakeReplayEvent(
TEXT("evt-002"),
2,
0,
EHyperTwistReplayEventType::TimerStart,
TEXT("")
),
MakeReplayEvent(
TEXT("evt-003"),
3,
200,
EHyperTwistReplayEventType::Move,
SerializeStructToJson(FirstMovePayload)
),
MakeReplayEvent(
TEXT("evt-004"),
4,
400,
EHyperTwistReplayEventType::Move,
SerializeStructToJson(SecondMovePayload)
),
MakeReplayEvent(
TEXT("evt-005"),
5,
700,
EHyperTwistReplayEventType::Move,
SerializeStructToJson(ThirdMovePayload)
),
MakeReplayEvent(
TEXT("evt-006"),
6,
1000,
EHyperTwistReplayEventType::SolveEnd,
TEXT("")
)
};
return ReplayPacket;
}
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistTwistyBound4VisualizationShellTest,
"HyperTwist.CleanRoom.TwistyJs.Bound4.VisualizationShell",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistTwistyBound4VisualizationShellTest::RunTest(const FString& Parameters)
{
UHyperTwistPuzzleViewerComponent* Viewer = NewObject<UHyperTwistPuzzleViewerComponent>();
TestNotNull(TEXT("Viewer component instance must be created."), Viewer);
TestTrue(
TEXT("Replay packet load must succeed for the Bound 4 visualization-shell test."),
Viewer->LoadReplayPacket(HyperTwistTwistyBound4VisualizationTestInternal::MakeReplayPacket())
);
FHyperTwistViewerVisualizationShellRequest Request;
Request.HostBootstrapRequest.HostInstanceId = TEXT("visual-host-01");
Request.HostBootstrapRequest.BindingMode = EHyperTwistViewerEmbeddedHostBindingMode::ScissoredView;
Request.HostBootstrapRequest.bBindTimelineSurface = true;
Request.HostBootstrapRequest.bBindScrubberSurface = true;
Request.HostBootstrapRequest.bBindStatusSurface = true;
Request.HostBootstrapRequest.bAllowPointerInput = true;
Request.VisualizationPreference = EHyperTwistViewerVisualizationPreference::Prefer3D;
Request.InitialPresentationMode = EHyperTwistViewerPresentationMode::Expanded;
Request.bAllow2DFallback = true;
Request.bAllowExpandedView = true;
Request.bAllowFullscreen = true;
FHyperTwistViewerVisualizationShellState VisualizationState;
TestTrue(
TEXT("The viewer must prepare a bounded visualization shell."),
Viewer->TryBuildVisualizationShell(Request, VisualizationState)
);
TestTrue(TEXT("The visualization shell state must be structurally valid."), VisualizationState.IsStructurallyValid());
TestEqual(TEXT("The visualization shell should preserve the bootstrap id."), VisualizationState.BootstrapId, Viewer->GetHostBootstrapState().BootstrapId);
TestEqual(TEXT("The visualization shell should preserve the scene context id."), VisualizationState.SceneContextId, Viewer->GetSceneContext().SceneContextId);
TestEqual(TEXT("The visualization shell should preserve the classic-cubing viewer adapter contract."), VisualizationState.ViewerAdapterContractId, FString(TEXT("classic-cubing-viewer-adapter-stack")));
TestEqual(TEXT("The visualization shell should reference the browser viewer embed contract."), VisualizationState.BrowserEmbedContractId, FString(TEXT("browser-viewer-embed")));
TestEqual(TEXT("The visualization shell should reference the browser spatial scene contract."), VisualizationState.SpatialSceneContractId, FString(TEXT("browser-spatial-scene-stack")));
TestEqual(TEXT("The visualization shell should reference the browser spatial UI contract."), VisualizationState.SpatialUiContractId, FString(TEXT("browser-spatial-ui-stack")));
TestEqual(TEXT("The visualization shell should reference the browser effect boundary."), VisualizationState.SpatialEffectBoundaryId, FString(TEXT("browser-spatial-effect-boundary")));
TestEqual(TEXT("The visualization shell should reference the browser state-motion contract."), VisualizationState.SpatialStateMotionContractId, FString(TEXT("browser-spatial-state-motion-stack")));
TestEqual(TEXT("The preferred visualization mode should stay 3D."), VisualizationState.PreferredMode, EHyperTwistViewerVisualizationMode::ThreeD);
TestEqual(TEXT("The active visualization mode should stay 3D when the support stack is available."), VisualizationState.ActiveMode, EHyperTwistViewerVisualizationMode::ThreeD);
TestFalse(TEXT("The 3D shell should not require a fallback in the scissored-view path."), VisualizationState.bUsingFallback);
TestTrue(TEXT("The 3D shell should mark spatial rendering active."), VisualizationState.bUsesSpatialRendering);
TestEqual(TEXT("The requested expanded presentation mode should be preserved."), VisualizationState.PresentationMode, EHyperTwistViewerPresentationMode::Expanded);
TestTrue(TEXT("The visualization shell should support fullscreen when requested."), VisualizationState.bSupportsFullscreen);
TestTrue(TEXT("The visualization shell should support expanded mode when requested."), VisualizationState.bSupportsExpandedView);
TestTrue(TEXT("The visualization shell should surface embeddable browser-viewer behavior tags."), VisualizationState.ViewerBehaviorTags.Contains(TEXT("embeddable-viewer")));
TestTrue(TEXT("The visualization shell should surface browser-viewer annotation tags."), VisualizationState.ViewerAnnotationSurfaceTags.Contains(TEXT("focus-annotation")));
TestTrue(TEXT("The visualization shell should surface world-anchored UI tags."), VisualizationState.SpatialUiStateSurfaceTags.Contains(TEXT("HTML/spatial overlay bridge")));
TestTrue(TEXT("The visualization shell should surface browser scene event tags."), VisualizationState.SpatialSceneEventSurfaceTags.Contains(TEXT("scissored view embedding")));
TestTrue(TEXT("The visualization shell should surface effect-family tags."), VisualizationState.SpatialEffectFamilies.Contains(TEXT("tone mapping and highlight effects")));
TestTrue(TEXT("The visualization shell should surface motion-kind tags."), VisualizationState.MotionKinds.Contains(TEXT("reduced-motion support")));
TestEqual(TEXT("The stored visualization shell state should match the returned visualization id."), Viewer->GetVisualizationShellState().VisualizationId, VisualizationState.VisualizationId);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FHyperTwistTwistyBound4FallbackPolicyTest,
"HyperTwist.CleanRoom.TwistyJs.Bound4.FallbackPolicy",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)
bool FHyperTwistTwistyBound4FallbackPolicyTest::RunTest(const FString& Parameters)
{
UHyperTwistPuzzleViewerComponent* Viewer = NewObject<UHyperTwistPuzzleViewerComponent>();
TestNotNull(TEXT("Viewer component instance must be created."), Viewer);
TestTrue(
TEXT("Replay packet load must succeed for the Bound 4 fallback-policy test."),
Viewer->LoadReplayPacket(HyperTwistTwistyBound4VisualizationTestInternal::MakeReplayPacket())
);
FHyperTwistViewerVisualizationShellRequest Request;
Request.HostBootstrapRequest.HostInstanceId = TEXT("visual-host-02");
Request.HostBootstrapRequest.BindingMode = EHyperTwistViewerEmbeddedHostBindingMode::InlineCanvas;
Request.HostBootstrapRequest.bBindTimelineSurface = true;
Request.HostBootstrapRequest.bBindScrubberSurface = true;
Request.HostBootstrapRequest.bBindStatusSurface = true;
Request.HostBootstrapRequest.bAllowPointerInput = true;
Request.VisualizationPreference = EHyperTwistViewerVisualizationPreference::Prefer3D;
Request.InitialPresentationMode = EHyperTwistViewerPresentationMode::Inline;
Request.bAllow2DFallback = true;
Request.bAllowExpandedView = true;
Request.bAllowFullscreen = true;
FHyperTwistViewerVisualizationShellState VisualizationState;
TestTrue(
TEXT("The viewer must still build a bounded visualization shell when 3D falls back to 2D."),
Viewer->TryBuildVisualizationShell(Request, VisualizationState)
);
TestTrue(TEXT("The fallback visualization shell state must be structurally valid."), VisualizationState.IsStructurallyValid());
TestEqual(TEXT("The preferred visualization mode should remain 3D."), VisualizationState.PreferredMode, EHyperTwistViewerVisualizationMode::ThreeD);
TestEqual(TEXT("Inline-canvas host binding should fall back to the 2D shell."), VisualizationState.ActiveMode, EHyperTwistViewerVisualizationMode::TwoD);
TestTrue(TEXT("The 2D shell should report that it is using the shared fallback policy."), VisualizationState.bUsingFallback);
TestEqual(
TEXT("The fallback reason should identify the host-binding constraint."),
VisualizationState.FallbackReason,
EHyperTwistViewerVisualizationFallbackReason::HostBindingDoesNotSupport3D
);
TestFalse(TEXT("The fallback 2D shell should not mark spatial rendering active."), VisualizationState.bUsesSpatialRendering);
TestEqual(TEXT("The initial inline presentation mode should be preserved."), VisualizationState.PresentationMode, EHyperTwistViewerPresentationMode::Inline);
FHyperTwistViewerVisualizationShellState FullscreenState;
TestTrue(
TEXT("Fullscreen should remain a visualization-local concern even on the 2D fallback shell."),
Viewer->TrySetVisualizationPresentationMode(EHyperTwistViewerPresentationMode::Fullscreen, FullscreenState)
);
TestEqual(TEXT("The presentation mode should move to fullscreen."), FullscreenState.PresentationMode, EHyperTwistViewerPresentationMode::Fullscreen);
TestEqual(TEXT("Fullscreen should preserve the active 2D fallback mode."), FullscreenState.ActiveMode, EHyperTwistViewerVisualizationMode::TwoD);
FHyperTwistViewerVisualizationShellState ExpandedState;
TestTrue(
TEXT("Expanded view should also remain a visualization-local concern."),
Viewer->TrySetVisualizationPresentationMode(EHyperTwistViewerPresentationMode::Expanded, ExpandedState)
);
TestEqual(TEXT("The presentation mode should move to expanded."), ExpandedState.PresentationMode, EHyperTwistViewerPresentationMode::Expanded);
TestEqual(TEXT("Expanded view should preserve the active 2D fallback mode."), ExpandedState.ActiveMode, EHyperTwistViewerVisualizationMode::TwoD);
TestEqual(TEXT("The stored visualization shell state should now reflect the expanded mode."), Viewer->GetVisualizationShellState().PresentationMode, EHyperTwistViewerPresentationMode::Expanded);
return true;
}
#endif // WITH_AUTOMATION_TESTS