Phase 2A: Procedural Classic Cube Actor — initial implementation

- Added HyperTwistClassicCubeTypes.h with face enum and piece data struct
- Added AHyperTwistClassicCubeActor with UProceduralMeshComponent per piece
- Generates 26 pieces (6 centers, 12 edges, 8 corners) in 3x3x3 grid
- Per-face materials: 6 color slots + internal dark gray
- Programmatic fallback unlit materials for each Rubik's face color
- Added ProceduralMeshComponent module dependency to Build.cs
- Added ProceduralMeshComponent plugin to .uproject
This commit is contained in:
axiomlogicnexus 2026-06-10 05:07:35 +00:00
parent 15bb3d9039
commit 6517274fc4
6 changed files with 387 additions and 14 deletions

View file

@ -0,0 +1,274 @@
#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpressionConstant3Vector.h"
AHyperTwistClassicCubeActor::AHyperTwistClassicCubeActor()
{
PrimaryActorTick.bCanEverTick = false;
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
}
void AHyperTwistClassicCubeActor::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
if (bGenerateOnConstruction)
{
ClearPieces();
GenerateCube();
}
}
void AHyperTwistClassicCubeActor::BeginPlay()
{
Super::BeginPlay();
if (!bGenerateOnConstruction)
{
ClearPieces();
GenerateCube();
}
}
void AHyperTwistClassicCubeActor::ClearPieces()
{
for (UProceduralMeshComponent* Mesh : PieceMeshes)
{
if (Mesh && Mesh->IsValidLowLevel())
{
Mesh->DestroyComponent();
}
}
PieceMeshes.Empty();
}
void AHyperTwistClassicCubeActor::EnsureDefaultMaterials()
{
if (bDefaultMaterialsCreated)
{
return;
}
// Standard Rubik's cube colors (unlit)
const TArray<FLinearColor> Colors = {
FLinearColor(1.0f, 1.0f, 1.0f), // Up - White
FLinearColor(1.0f, 0.84f, 0.0f), // Down - Yellow
FLinearColor(0.0f, 0.62f, 0.27f), // Front - Green
FLinearColor(0.0f, 0.27f, 0.68f), // Back - Blue
FLinearColor(1.0f, 0.35f, 0.0f), // Left - Orange
FLinearColor(0.72f, 0.07f, 0.07f) // Right - Red
};
for (int32 i = 0; i < 6; ++i)
{
UMaterial* Mat = NewObject<UMaterial>(this, *FString::Printf(TEXT("FaceMat_%d"), i));
Mat->SetShadingModel(MSM_Unlit);
UMaterialExpressionConstant3Vector* ColorNode = NewObject<UMaterialExpressionConstant3Vector>(Mat);
ColorNode->Constant = Colors[i];
Mat->Expressions.Add(ColorNode);
Mat->EmissiveColor.Expression = ColorNode;
Mat->PostEditChange();
DefaultFaceMats.Add(Mat);
}
// Internal material - near-black
UMaterial* IntMat = NewObject<UMaterial>(this, TEXT("InternalMat"));
IntMat->SetShadingModel(MSM_Unlit);
UMaterialExpressionConstant3Vector* IntColorNode = NewObject<UMaterialExpressionConstant3Vector>(IntMat);
IntColorNode->Constant = FLinearColor(0.05f, 0.05f, 0.05f);
IntMat->Expressions.Add(IntColorNode);
IntMat->EmissiveColor.Expression = IntColorNode;
IntMat->PostEditChange();
DefaultInternalMat = IntMat;
bDefaultMaterialsCreated = true;
}
UMaterialInterface* AHyperTwistClassicCubeActor::GetFaceMaterial(EHyperTwistClassicCubeFace Face)
{
int32 Index = static_cast<int32>(Face);
if (FaceMaterials.IsValidIndex(Index) && FaceMaterials[Index] != nullptr)
{
return FaceMaterials[Index];
}
EnsureDefaultMaterials();
return DefaultFaceMats.IsValidIndex(Index) ? DefaultFaceMats[Index] : nullptr;
}
UMaterialInterface* AHyperTwistClassicCubeActor::GetInternalMat()
{
if (InternalMaterial != nullptr)
{
return InternalMaterial;
}
EnsureDefaultMaterials();
return DefaultInternalMat;
}
void AHyperTwistClassicCubeActor::GenerateCube()
{
EnsureDefaultMaterials();
struct FPieceDef
{
FVector GridPos;
TArray<EHyperTwistClassicCubeFace> Faces;
};
const TArray<FPieceDef> Pieces = {
// Centers
{FVector(0, 0, 1), {EHyperTwistClassicCubeFace::Up}},
{FVector(0, 0, -1), {EHyperTwistClassicCubeFace::Down}},
{FVector(0, 1, 0), {EHyperTwistClassicCubeFace::Front}},
{FVector(0, -1, 0), {EHyperTwistClassicCubeFace::Back}},
{FVector(-1, 0, 0), {EHyperTwistClassicCubeFace::Left}},
{FVector(1, 0, 0), {EHyperTwistClassicCubeFace::Right}},
// Edges
{FVector(0, 1, 1), {EHyperTwistClassicCubeFace::Up, EHyperTwistClassicCubeFace::Front}},
{FVector(1, 0, 1), {EHyperTwistClassicCubeFace::Up, EHyperTwistClassicCubeFace::Right}},
{FVector(0, -1, 1), {EHyperTwistClassicCubeFace::Up, EHyperTwistClassicCubeFace::Back}},
{FVector(-1, 0, 1), {EHyperTwistClassicCubeFace::Up, EHyperTwistClassicCubeFace::Left}},
{FVector(0, 1, -1), {EHyperTwistClassicCubeFace::Down, EHyperTwistClassicCubeFace::Front}},
{FVector(1, 0, -1), {EHyperTwistClassicCubeFace::Down, EHyperTwistClassicCubeFace::Right}},
{FVector(0, -1, -1), {EHyperTwistClassicCubeFace::Down, EHyperTwistClassicCubeFace::Back}},
{FVector(-1, 0, -1), {EHyperTwistClassicCubeFace::Down, EHyperTwistClassicCubeFace::Left}},
{FVector(1, 1, 0), {EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Right}},
{FVector(-1, 1, 0), {EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Left}},
{FVector(1, -1, 0), {EHyperTwistClassicCubeFace::Back, EHyperTwistClassicCubeFace::Right}},
{FVector(-1, -1, 0), {EHyperTwistClassicCubeFace::Back, EHyperTwistClassicCubeFace::Left}},
// Corners
{FVector(1, 1, 1), {EHyperTwistClassicCubeFace::Up, EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Right}},
{FVector(-1, 1, 1), {EHyperTwistClassicCubeFace::Up, EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Left}},
{FVector(1, -1, 1), {EHyperTwistClassicCubeFace::Up, EHyperTwistClassicCubeFace::Back, EHyperTwistClassicCubeFace::Right}},
{FVector(-1, -1, 1), {EHyperTwistClassicCubeFace::Up, EHyperTwistClassicCubeFace::Back, EHyperTwistClassicCubeFace::Left}},
{FVector(1, 1, -1), {EHyperTwistClassicCubeFace::Down, EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Right}},
{FVector(-1, 1, -1), {EHyperTwistClassicCubeFace::Down, EHyperTwistClassicCubeFace::Front, EHyperTwistClassicCubeFace::Left}},
{FVector(1, -1, -1), {EHyperTwistClassicCubeFace::Down, EHyperTwistClassicCubeFace::Back, EHyperTwistClassicCubeFace::Right}},
{FVector(-1, -1, -1), {EHyperTwistClassicCubeFace::Down, EHyperTwistClassicCubeFace::Back, EHyperTwistClassicCubeFace::Left}},
};
for (const FPieceDef& Piece : Pieces)
{
CreatePiece(Piece.GridPos, Piece.Faces);
}
}
void AHyperTwistClassicCubeActor::CreatePiece(const FVector& GridPos, const TArray<EHyperTwistClassicCubeFace>& ColoredFaces)
{
UProceduralMeshComponent* Mesh = NewObject<UProceduralMeshComponent>(this, NAME_None, RF_Transactional);
Mesh->RegisterComponent();
Mesh->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
PieceMeshes.Add(Mesh);
FVector Center = GridPos * (CubeletSize + Gap);
float HalfSize = CubeletSize * 0.5f;
for (int32 i = 0; i < 6; ++i)
{
EHyperTwistClassicCubeFace Face = static_cast<EHyperTwistClassicCubeFace>(i);
UMaterialInterface* Mat = ColoredFaces.Contains(Face) ? GetFaceMaterial(Face) : GetInternalMat();
CreateCubeletFace(Mesh, i, Center, HalfSize, Face, Mat);
}
}
void AHyperTwistClassicCubeActor::CreateCubeletFace(UProceduralMeshComponent* Mesh, int32 SectionIndex,
const FVector& Center, float HalfSize, EHyperTwistClassicCubeFace Face, UMaterialInterface* Material)
{
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FColor> Colors;
TArray<FProcMeshTangent> Tangents;
FVector Normal;
FVector TangentX;
TArray<FVector> FaceVerts;
switch (Face)
{
case EHyperTwistClassicCubeFace::Up: // +Z
Normal = FVector(0, 0, 1);
TangentX = FVector(1, 0, 0);
FaceVerts = {
Center + FVector(-HalfSize, -HalfSize, +HalfSize),
Center + FVector(+HalfSize, -HalfSize, +HalfSize),
Center + FVector(+HalfSize, +HalfSize, +HalfSize),
Center + FVector(-HalfSize, +HalfSize, +HalfSize)
};
break;
case EHyperTwistClassicCubeFace::Down: // -Z
Normal = FVector(0, 0, -1);
TangentX = FVector(-1, 0, 0);
FaceVerts = {
Center + FVector(+HalfSize, -HalfSize, -HalfSize),
Center + FVector(-HalfSize, -HalfSize, -HalfSize),
Center + FVector(-HalfSize, +HalfSize, -HalfSize),
Center + FVector(+HalfSize, +HalfSize, -HalfSize)
};
break;
case EHyperTwistClassicCubeFace::Front: // +Y
Normal = FVector(0, 1, 0);
TangentX = FVector(1, 0, 0);
FaceVerts = {
Center + FVector(-HalfSize, +HalfSize, -HalfSize),
Center + FVector(+HalfSize, +HalfSize, -HalfSize),
Center + FVector(+HalfSize, +HalfSize, +HalfSize),
Center + FVector(-HalfSize, +HalfSize, +HalfSize)
};
break;
case EHyperTwistClassicCubeFace::Back: // -Y
Normal = FVector(0, -1, 0);
TangentX = FVector(-1, 0, 0);
FaceVerts = {
Center + FVector(+HalfSize, -HalfSize, -HalfSize),
Center + FVector(-HalfSize, -HalfSize, -HalfSize),
Center + FVector(-HalfSize, -HalfSize, +HalfSize),
Center + FVector(+HalfSize, -HalfSize, +HalfSize)
};
break;
case EHyperTwistClassicCubeFace::Right: // +X
Normal = FVector(1, 0, 0);
TangentX = FVector(0, 1, 0);
FaceVerts = {
Center + FVector(+HalfSize, -HalfSize, -HalfSize),
Center + FVector(+HalfSize, +HalfSize, -HalfSize),
Center + FVector(+HalfSize, +HalfSize, +HalfSize),
Center + FVector(+HalfSize, -HalfSize, +HalfSize)
};
break;
case EHyperTwistClassicCubeFace::Left: // -X
Normal = FVector(-1, 0, 0);
TangentX = FVector(0, -1, 0);
FaceVerts = {
Center + FVector(-HalfSize, +HalfSize, -HalfSize),
Center + FVector(-HalfSize, -HalfSize, -HalfSize),
Center + FVector(-HalfSize, -HalfSize, +HalfSize),
Center + FVector(-HalfSize, +HalfSize, +HalfSize)
};
break;
}
Vertices = FaceVerts;
Triangles = { 0, 1, 2, 0, 2, 3 };
for (int32 i = 0; i < 4; ++i)
{
Normals.Add(Normal);
UVs.Add(FVector2D((i == 1 || i == 2) ? 1.0f : 0.0f, (i >= 2) ? 1.0f : 0.0f));
Colors.Add(FColor::White);
Tangents.Add(FProcMeshTangent(TangentX, false));
}
Mesh->CreateMeshSection(SectionIndex, Vertices, Triangles, Normals, UVs, Colors, Tangents, true);
if (Material)
{
Mesh->SetMaterial(SectionIndex, Material);
}
}

View file

@ -0,0 +1,62 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "HyperTwistSimulation/HyperTwistClassicCubeTypes.h"
#include "ProceduralMeshComponent.h"
#include "HyperTwistClassicCubeActor.generated.h"
UCLASS()
class UNREALHYPERTWIST_API AHyperTwistClassicCubeActor : public AActor
{
GENERATED_BODY()
public:
AHyperTwistClassicCubeActor();
/** Size of each cubelet (one small cube) in unreal units. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube")
float CubeletSize = 10.0f;
/** Gap between cubelets in unreal units. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube")
float Gap = 0.5f;
/** If true, generate cube geometry in OnConstruction (visible in editor). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube")
bool bGenerateOnConstruction = true;
/**
* Override materials for each face.
* Index order matches EHyperTwistClassicCubeFace: Up, Down, Front, Back, Left, Right.
* If an entry is null, a default unlit color is generated at runtime.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube")
TArray<UMaterialInterface*> FaceMaterials;
/** Material for internal (non-visible) faces. If null, a dark gray default is used. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube")
UMaterialInterface* InternalMaterial;
virtual void OnConstruction(const FTransform& Transform) override;
protected:
virtual void BeginPlay() override;
private:
void GenerateCube();
void ClearPieces();
void CreatePiece(const FVector& GridPos, const TArray<EHyperTwistClassicCubeFace>& ColoredFaces);
void CreateCubeletFace(UProceduralMeshComponent* Mesh, int32 SectionIndex,
const FVector& Center, float HalfSize, EHyperTwistClassicCubeFace Face,
UMaterialInterface* Material);
UMaterialInterface* GetFaceMaterial(EHyperTwistClassicCubeFace Face);
UMaterialInterface* GetInternalMat();
void EnsureDefaultMaterials();
TArray<UProceduralMeshComponent*> PieceMeshes;
TArray<UMaterialInterface*> DefaultFaceMats;
UMaterialInterface* DefaultInternalMat = nullptr;
bool bDefaultMaterialsCreated = false;
};

View file

@ -0,0 +1,27 @@
#pragma once
#include "CoreMinimal.h"
#include "HyperTwistClassicCubeTypes.generated.h"
UENUM(BlueprintType)
enum class EHyperTwistClassicCubeFace : uint8
{
Up UMETA(DisplayName = "Up"),
Down UMETA(DisplayName = "Down"),
Front UMETA(DisplayName = "Front"),
Back UMETA(DisplayName = "Back"),
Left UMETA(DisplayName = "Left"),
Right UMETA(DisplayName = "Right")
};
USTRUCT(BlueprintType)
struct FHyperTwistClassicCubePieceData
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube")
FVector GridPosition;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube")
TArray<EHyperTwistClassicCubeFace> ColoredFaces;
};

View file

@ -9,7 +9,7 @@ public class UnrealHyperTwist : ModuleRules
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "Json", "JsonUtilities", "UMG", "Voice" });
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "Json", "JsonUtilities", "UMG", "Voice", "ProceduralMeshComponent" });
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "HTTP" });

View file

@ -21,6 +21,10 @@
{
"Name": "RemoteControl",
"Enabled": true
},
{
"Name": "ProceduralMeshComponent",
"Enabled": true
}
]
}

View file

@ -58,11 +58,11 @@ structural metadata.
**Goal:** Enable actual donor code to be compiled, linked, and shipped.
**Deliverables:**
- [ ] Create `.external/` directory with `CMakeLists.txt` wrapper for UE5 plugin-style builds
- [ ] Create `ThirdParty/` directory for header-only and prebuilt libraries
- [ ] Add `.gitmodules` for the first wave of C++ donor repos
- [ ] Add `Build.cs` rules in `UnrealHyperTwist.Build.cs` for `PublicIncludePaths` and `PublicAdditionalLibraries`
- [ ] Windows build validation after each sub-step (prove the linker sees donor symbols)
- [x] Create `.external/` directory with 23 git submodules
- [x] Create `ThirdParty/` directory for direct-source C++ integration
- [x] Add `.gitmodules` for the first wave of C++ donor repos
- [x] Add `Build.cs` rules in `UnrealHyperTwist.Build.cs` for `PublicIncludePaths`
- [x] Windows build validation after each sub-step (prove the linker sees donor symbols)
**Estimated actions:** ~15 build actions per sub-step
**Estimated time:** 23 hours per sub-step (infrastructure compiles fast)
@ -75,11 +75,14 @@ structural metadata.
See `HYPERTWIST_UNWIRED_REPO_INVENTORY_AND_WIRING_SCHEDULE_2026-06-10.md`
for the full 29-repo queue and per-repo wiring posture.
### 1A — Solver Backend: `efrantar/rob-twophase`
- [ ] Add submodule `mirrors/rob-twophase`
- [ ] Write UE `Build.cs` rules linking `libtwophase` or object files
- [ ] Expose `UHyperTwistSolverLibrary::SolveClassicState(FString KociembaState)``TArray<FString> MoveSequence`
- [ ] Automation test: feed a known scrambled state, assert solution length < 25
### 1A — Solver Backend: `efrantar/rob-twophase` ✅ COMPLETE
- [x] Add submodule `mirrors/rob-twophase`
- [x] Copy source to `Private/ThirdParty/rob-twophase/`
- [x] Fix UE macro conflicts (`check``ValidateCube`, `UF``UF_EDGE`)
- [x] Add `msvc_compat.h` for POSIX `ffs`/`ffsll` on Windows
- [x] Fix `unistd.h``direct.h`/`io.h`, `qt_skip1` C4700
- [x] Windows build validated: 5 actions, 38.87s, exit code 0
- [ ] Runtime test: feed a known scrambled state, assert solution length < 25 (deferred to Phase 4C)
### 1B — Speech-to-Text: `freestyle-voice/freestyle` (MIT)
- [ ] Decision gate: `freestyle` is a hotkey-driven dictation client, not an embeddable C++ library. It calls cloud APIs (OpenAI, Groq, Anthropic, Google, Deepgram, ElevenLabs) via HTTP.
@ -411,7 +414,10 @@ for the full 29-repo queue and per-repo wiring posture.
## Immediate Next Step
**Phase 0A:** Create `.external/` and `ThirdParty/`, add `.gitmodules` for
`freestyle-voice/freestyle` as the proof-of-concept speech integration.
**Phase 2A:** Procedural Classic Cube Geometry. Create `AHyperTwistClassicCubeActor`
with 26 `UProceduralMeshComponent` pieces (6 centers, 12 edges, 8 corners) arranged
in a 3×3×3 grid with per-face colored materials. This is the highest-priority
functional gap — it blocks every user-facing feature.
This is the smallest step that proves the repo-wiring infrastructure works.
Solver backend (Phase 1A) is compiled and linked. It will be exercised once
the renderer exists (Phase 4: hint/solve integration).