This repo is a full-stack Rubik’s Cube toy:
- Backend (Spring Boot) holds the authoritative cube state and applies moves.
- Frontend server (Node/Express) serves the static Three.js UI and manages a
user_idcookie. - Frontend UI (Three.js + vanilla JS) renders a 3D cube, highlights the selected slice, and sends move commands.
There’s also a deployed version (may be offline):
backend/— Spring Boot serviceCube.java— the cube model + rotation logicCubeController.java— REST endpoints + in-memory session repositoryCubeserviceApplication.java— Spring Boot entry pointpom.xml— Maven build
frontend/— Express server + Three.js clientcubecontroller.js— Express server, cookie handling, proxy to Spring BootcubeGUI.html— UI shellscene.js,cubemanagement.js,animation.js— Three.js rendering + cube face mapping + win animationeventhandler.js— keyboard + buttonsdatapaths.js—fetchCube()/fetchCommand()helpers
You’ll run two processes: Spring Boot backend and Express frontend.
From backend/:
mvn spring-boot:runBy default this will come up on port 3000 (based on how the frontend is configured).
Note: I haven’t run this repo since 2023, so you may need to adjust ports / CORS / base URLs and project dependencies.
From frontend/:
npm install
npm startThen open:
The UI uses the same pointer concept as an older Swing prototype (not included on this branch).
w/s— apply U/D (or HU/HD in horizontal mode) or move the pointer depending on modea/d— move pointer or apply L/R depending on modespace— toggle pointer axis (pointer.x0 ↔ 1)h— toggle horizontal control (pointer.horizontal)
Camera:
- Arrow keys rotate the camera.
Toolbar:
- Clear resets to solved
- Shuffle scrambles (difficulty 1–3)
- How To opens help popup
The Express server exposes simple routes used by the browser:
GET /fetch-data- creates a
user_idcookie if missing - returns JSON cube state
- creates a
POST /send-command/:commandType- body:
{ "index": <0..2> } - forwards the command to Spring Boot
- body:
Spring Boot endpoints (called by Express):
GET /cube/fetch-data?user_id=...POST /cube/command/?user_id=...&move=...&pointer=...
Supported move values:
U, D, L, R, HU, HD, shuffle1, shuffle2, shuffle3, clear
backend/Cube.java models the cube as 6 faces, each an int[3][3].
- Face ids are fixed:
0 FRONT,1 RIGHT,2 BACK,3 LEFT,4 UP,5 DOWN
facesis anArrayList<int[][]]wherefaces.get(id)returns that face.
The backend returns these matrices as JSON fields like frontFace, upFace, etc.
Design note / simplification idea: the current representation is sticker-based (6 independent faces). For a more scalable model (4×4+, solvers, cleaner rotations), consider a cubie-based model:
- Represent corner/edge/center cubies with positions + orientations.
- Moves become permutations + orientation tweaks.
- Frontend mapping becomes simpler because you can render cubies directly.
This would require a rendering rework, but it improves maintainability long-term.
As in the Swing prototype, each move is implemented in two parts:
- Remap a strip (row/column) across 4 adjacent faces.
- Rotate an end face if the strip is on an outer layer.
planeRotation(face, direction) does a standard in-place 90° matrix rotation using a copied face buffer.
Row cycles follow:
ROT_ROW = { FRONT, RIGHT, BACK, LEFT, FRONT }
Key detail in this branch: when mapping a row across the BACK face, the index and/or order must be reflected.
The code computes a backreflectIndex and uses 2 - j in certain steps to keep orientation correct.
Column cycles depend on mode:
- Normal:
ROT_COLUMN_FACE = { FRONT, UP, BACK, DOWN, FRONT } - Horizontal:
ROT_COLUMN_SIDE = { LEFT, UP, RIGHT, DOWN, LEFT }
Key detail in this branch: the horizontal mapping uses a columnLatch that alternates between two assignment styles,
plus precomputed indexes to correctly mirror the strip when moving between faces with different orientation.
scramble(level) performs a random walk, but in this branch the number of random moves scales as:
That means difficulty grows quickly: shuffle1=1, shuffle2=8, shuffle3=27 random moves.
The backend face matrices are in a consistent logical orientation, but the Three.js face groups
(groups.front, groups.left, etc.) are populated based on cubie creation order and material index layout.
To compensate, frontend/cubemanagement.js applies hard-coded transforms like rotateArray() and reflectArray()
before calling updateFace(...).
That’s why you see code like:
- rotate/reflect of
frontFace,leftFace,upFace, …
These are rendering-space transforms and don’t change the cube logic.
I created this project after my first Object-Oriented-Programming Course at UW-Madison in 2023. This was one of my first self lead, full stack applications. Improvements at the architechtural level in design choices: Switch to Python framework - Better aiding in OOP and overall application centralization. Containzerize the application and get CI/CD going Implement the actual algorithm to solve the cube - currently replays the random walk as the solution. Could use this further to devlop instructional GUI components.
- Fix
isSolved()logic: as written it resetsfirstNumevery timej==0, which makes the check weaker than intended. Usually you want to compare every sticker on a face to that face’s first sticker (or the face id). (Probably fine in practice, but worth fixing for consistency.) - Introduce a real move representation: define an enum for moves (
U,D, …) and validate centrally. - Thread safety:
cubeRepository/userIdSessionsare plainHashMaps. Under concurrent requests, this can race. ConsiderConcurrentHashMapand safer expiry iteration. - Session storage: in-memory maps are fine for a demo, but a restart wipes sessions. A small persistence layer (Redis) would scale.
- Base URL mismatch / configuration:
frontend/cubecontroller.jssetsaxios.defaults.baseURL = 'http://localhost:3000'. That’s great locally if Spring runs on 3000, but should be environment-driven (env var) for deployment. - Error handling:
fetchCommand()callsupdateCubeGraphics(response.json())without awaiting; make the promise flow explicit. - Input model:
pointer.xis used as a mode flag (0/1). Naming itaxisMode(or similar) would improve readability.
- Standard notation: consider adding conventional Singmaster turns and inverse moves (
U',R', etc.) on the CubeInterface level to simplify the graphical translation. - Tests: add unit tests for invariants (e.g., move repeated 4× returns to original state).
- The rotate/reflect transforms are currently “magic constants”. A longer-term improvement is to define a single canonical coordinate system and derive transforms from it.