From 6517274fc4fe9ddf9fe8f381d95f787ebe9e4dc5 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Wed, 10 Jun 2026 05:07:35 +0000 Subject: [PATCH] =?UTF-8?q?Phase=202A:=20Procedural=20Classic=20Cube=20Act?= =?UTF-8?q?or=20=E2=80=94=20initial=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../HyperTwistClassicCubeActor.cpp | 274 ++++++++++++++++++ .../HyperTwistClassicCubeActor.h | 62 ++++ .../HyperTwistClassicCubeTypes.h | 27 ++ .../UnrealHyperTwist.Build.cs | 2 +- UnrealHyperTwist/UnrealHyperTwist.uproject | 4 + ...NSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md | 32 +- 6 files changed, 387 insertions(+), 14 deletions(-) create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeActor.cpp create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeActor.h create mode 100644 UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeTypes.h diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeActor.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeActor.cpp new file mode 100644 index 0000000..b3c4a5a --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeActor.cpp @@ -0,0 +1,274 @@ +#include "HyperTwistSimulation/HyperTwistClassicCubeActor.h" +#include "Materials/Material.h" +#include "Materials/MaterialExpressionConstant3Vector.h" + +AHyperTwistClassicCubeActor::AHyperTwistClassicCubeActor() +{ + PrimaryActorTick.bCanEverTick = false; + RootComponent = CreateDefaultSubobject(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 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(this, *FString::Printf(TEXT("FaceMat_%d"), i)); + Mat->SetShadingModel(MSM_Unlit); + + UMaterialExpressionConstant3Vector* ColorNode = NewObject(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(this, TEXT("InternalMat")); + IntMat->SetShadingModel(MSM_Unlit); + UMaterialExpressionConstant3Vector* IntColorNode = NewObject(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(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 Faces; + }; + + const TArray 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& ColoredFaces) +{ + UProceduralMeshComponent* Mesh = NewObject(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(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 Vertices; + TArray Triangles; + TArray Normals; + TArray UVs; + TArray Colors; + TArray Tangents; + + FVector Normal; + FVector TangentX; + TArray 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); + } +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeActor.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeActor.h new file mode 100644 index 0000000..ca713e7 --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeActor.h @@ -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 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& 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 PieceMeshes; + TArray DefaultFaceMats; + UMaterialInterface* DefaultInternalMat = nullptr; + bool bDefaultMaterialsCreated = false; +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeTypes.h new file mode 100644 index 0000000..3ea3a7c --- /dev/null +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeTypes.h @@ -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 ColoredFaces; +}; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/UnrealHyperTwist.Build.cs b/UnrealHyperTwist/Source/UnrealHyperTwist/UnrealHyperTwist.Build.cs index fcf4491..4738536 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/UnrealHyperTwist.Build.cs +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/UnrealHyperTwist.Build.cs @@ -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" }); diff --git a/UnrealHyperTwist/UnrealHyperTwist.uproject b/UnrealHyperTwist/UnrealHyperTwist.uproject index 1c1968c..b35e73c 100644 --- a/UnrealHyperTwist/UnrealHyperTwist.uproject +++ b/UnrealHyperTwist/UnrealHyperTwist.uproject @@ -21,6 +21,10 @@ { "Name": "RemoteControl", "Enabled": true + }, + { + "Name": "ProceduralMeshComponent", + "Enabled": true } ] } \ No newline at end of file diff --git a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md index 8ca09f0..bed0497 100644 --- a/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md +++ b/docs/ops/HYPERTWIST_COMPREHENSIVE_RECONSTRUCTION_ROADMAP_2026-06-10.md @@ -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:** 2–3 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 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).