oneshotlm
Compare all models on this prompt →

Rubik's Cube · glm-5.2

Using the pre-provided three.min.js (global THREE), render an interactive 3D Rubik's Cube with clickable face rotations, orbit-drag camera, and a scramble button.

⛨ sandboxed Open in new tab ↗
transcript · 8 messages
user

Using the pre-provided three.min.js (global THREE), render an interactive 3D Rubik's Cube with clickable face rotations, orbit-drag camera, and a scramble button.

assistant
thinking
Let me check what's in the current directory first.
bash {"command":"ls -la /workspace"}
toolResult

total 644 drwxrwxrwx 3 node node 96 Jul 10 00:31 . drwxr-xr-x 1 root root 4096 Jul 10 00:31 .. -rw-r--r-- 1 node node 603445 Jul 10 00:31 three.min.js

assistant
thinking
Let me build a Rubik's cube app. I'll create an interactive 3D Rubik's cube with:
- 27 cubies arranged in a 3x3x3 grid
- Clickable face rotations (click a face to rotate that slice)
- Orbit-drag camera (I'll implement my own orbit controls since OrbitControls may not be in three.min.js)
- A scramble button

Let me write a single index.html file.

Key design:
- Build 27 cubies as THREE.Mesh with BoxGeometry, each face colored appropriately. Actually easier: make each cubie a group of 6 colored plane meshes, or use a single BoxGeometry with multi-material (6 materials, one per face).
- For each cubie at position (x,y,z) where x,y,z ∈ {-1,0,1}, assign face colors based on which face is outward.
- Face colors: +X right=red, -X left=orange, +Y up=white, -Y down=yellow, +Z front=green, -Z back=blue.

For rotation: when user clicks a face, determine which slice to rotate. I'll implement picking by raycasting. On click, determine the clicked face normal and the cubie position. The axis is the normal direction, and the slice is the cubie's coordinate along that axis.

Animation: create a pivot group, attach the 9 cubies of that slice, rotate the pivot by 90 degrees over time, then re-parent cubies back to scene and snap positions/rotations.

Camera orbit: implement custom orbit with mouse drag to rotate camera around target, wheel to zoom.

Scramble button: perform a sequence of random moves quickly.

Let me write the code.

For click vs drag detection: track mousedown position, if mouse moved beyond threshold it's a drag (orbit), otherwise it's a click (face rotation).

Let me code this carefully.

I'll structure:

```js
// scene, camera, renderer
// cubies array
// build cube
// animation queue
// picking
// orbit controls
// scramble
```

Let me write it.

Colors (standard Rubik): 
- White (U, +Y): 0xffffff
- Yellow (D, -Y): 0xffff00
- Red (R, +X): 0xff0000... actually right is red? Standard: White U, Yellow D, Green F, Blue B, Red R, Orange L. Let me use: R=red 0xB90000, L=orange 0xFF5900, U=white 0xFFFFFF, D=yellow 0xFFD500, F=green 0x0046AD... wait green is F? Actually standard western: White opposite Yellow, Red opposite Orange, Blue opposite Green. F=Green, B=Blue. Let me use:
- +X (R): red 0xC41E3A
- -X (L): orange 0xFF5800
- +Y (U): white 0xFFFFFF
- -Y (D): yellow 0xFFD500
- +Z (F): green 0x009E60
- -Z (B): blue 0x0051BA

Black for inner faces (not outward): 0x111111.

For each cubie, materials array order for BoxGeometry: [px, nx, py, ny, pz, nz] = [right, left, top, bottom, front, back].

For cubie at (x,y,z) (each -1..1):
- material[0] (px, +X): if x==1 then red else black
- material[1] (nx, -X): if x==-1 then orange else black
- material[2] (py, +Y): if y==1 then white else black
- material[3] (ny, -Y): if y==-1 then yellow else black
- material[4] (pz, +Z): if z==1 then green else black
- material[5] (nz, -Z): if z==-1 then blue else black

Cubie size: 0.95 with gaps, or 1.0 with small gap. Let me use size 0.96 and round corners via a slightly larger black box behind. Simpler: cubie size 0.95, gap visible. Position spacing 1.0.

Add edges? Optional. Let me add black wireframe edges for nice look. Actually I'll add a black box slightly larger behind colored stickers. Keep simple: just colored box.

Let me make stickers: for a nicer look, create the cubie as a black rounded box (use BoxGeometry 0.95) and add colored sticker planes on outward faces. But simpler approach with multi-material is fine and looks good. Let me go with multi-material black for inner faces.

Actually for the classic look with black gaps, I'll make each cubie a black box of size 1.0 with colored sticker meshes inset on outward faces. This gives nice black borders. Let me do that.

Plan per cubie:
- blackBox: BoxGeometry(0.95, 0.95, 0.95) MeshStandardMaterial black
- stickers: for each outward face, a PlaneGeometry(0.8, 0.8) colored, positioned just outside the box face (0.001 offset).

Group them.

Hmm but then rotation by slice needs to move the group. That's fine—cubie is the Group.

Let me use a Group per cubie, add to scene. Track in cubies array with logical position.

Rotation implementation:
```
function rotateSlice(axis, layer, dir) {
  // axis: 'x'|'y'|'z', layer: -1|0|1, dir: +1 or -1 (90deg)
  // gather cubies whose position[axis] == layer
  const pivot = new THREE.Group();
  scene.add(pivot);
  const sliceCubies = cubies.filter(c => Math.round(c.position[axis]) === layer);
  sliceCubies.forEach(c => pivot.attach(c));
  // animate
  const angle = dir * Math.PI/2;
  animate rotation of pivot on axis to angle
  // then detach
  sliceCubies.forEach(c => {
    scene.attach(c);
    // round position and rotation
  });
  pivot.remove... scene.remove(pivot)
}
```

Use a tween via requestAnimationFrame. I'll maintain an animation state object with startTime, duration, and on complete do the reattach.

Queue moves to handle scramble (sequential).

Let me write the full code now.

For orbit controls: 
- on mousedown record start
- on mousemove if dragging, rotate camera around target
- spherical coords theta, phi, radius
- update camera position from spherical
- wheel to zoom radius

For click detection: on mouseup, if movement small and no animation, do raycast pick.

When pick hits a sticker, the sticker has userData with face normal (the outward direction) and we know cubie. Determine axis = the normal axis (e.g., normal (1,0,0) -> axis 'x', layer = cubie's x). Direction: a click rotates that face clockwise from the viewer's perspective. I'll just pick a consistent dir based on which side. Let me compute dir from the relationship between camera view and normal. Simpler: dir = +1 always for now, but better to make it feel natural: rotating the clicked face in the direction of the drag would be ideal but complex. Let me just do a fixed direction but allow the other direction via a modifier... Actually, let me make the rotation direction depend on the click position relative to center of the face? Keep it simple: each click rotates that face 90° in a fixed direction; the user can keep clicking. Good enough for the requirement "clickable face rotations."

Actually a nicer behavior: clicking on a face rotates that face by 90 degrees, and the direction is determined by which axis and the sign. Let me just always rotate so the clicked face turns clockwise when viewed from outside. Clockwise from outside means: looking along the inward normal (from outside toward center). For +X face viewed from +X looking toward -X, clockwise rotation around +X axis is... Let me just pick dir based on axis sign: for outward normal n, dir = +1 if axis sign positive else -1, times a per-axis factor. I'll just hardcode: rotate around the axis by -90°*sign. Let me just choose a direction and not overthink; users can scramble and click to rotate.

Hmm, let me make it more interesting: direction determined by the face and whether it's an outer face. For middle layers (layer 0), clicking a sticker on a middle cubie... but stickers only exist on outer faces, so the clicked sticker is always an outer face (layer ±1). So we only rotate outer layers via clicks. Middle layers only via... well there's no middle sticker. That's fine—typical Rubik apps rotate outer faces only on click. Good.

So: clicked sticker -> normal axis & sign -> layer = sign (±1) -> rotate that outer slice. dir: pick so it looks like clockwise from outside. Clockwise viewed from outside along -normal direction (looking inward). For axis X positive outward (normal +X), viewer at +X looking toward -X. Clockwise rotation in that view corresponds to rotation about +X by +90°? Let's verify: rotation about +X by +90° maps +Y->+Z, +Z->-Y. Viewer at +X looking along -X, screen up = +Y, screen right = -Z (since right = up cross view-dir... view dir into screen = -X, up=+Y, right = up × forward... let me not). I'll just pick dir and if it feels off it's still a valid cube. dir = sign. Good enough.

Let me code.

For snapping after rotation: round each cubie position component to nearest integer, and round rotation. Easier: after pivot rotation, the cubie's local matrix is updated; attach to scene via scene.attach which preserves world transform. Then round position: Math.round. For rotation, the cumulative quaternion may drift; I can snap the cubie's rotation to nearest 90° by converting. Simplest robust approach: store the logical state and recompute? That's more work.

Alternative robust approach: Track each cubie's orientation as a quaternion that's always a multiple of 90°, and position as integer. After each move, compute new positions/orientations logically without floating tween on the cubie transform; instead tween a pivot containing copies? Too complex.

Practical approach: tween the pivot rotation, and on completion snap by recomputing. Let me snap positions with Math.round (works since 90° rotations of integer positions yield integer positions). For rotation, set cubie.quaternion from a rounded euler or just leave the float quaternion which after 90° increments stays clean enough (floating error small). Over many scrambles error could accumulate but rounding position handles grid. Quaternion drift: I'll snap quaternion by rounding each component? Quaternions for 90° multiples have components in {0, ±0.7071, ±1}. I can snap by finding nearest. Let me implement a snapQuaternion: round each of x,y,z,w to nearest of {-1,-0.7071,0,0.7071,1} then normalize. That keeps orientation clean.

Actually simpler and bulletproof: Don't tween individual cubie orientations persistently. Instead, represent each cubie's orientation by applying the move to a stored quaternion, and animate a SEPARATE display. Hmm.

Let me just do the pivot approach and snap quaternion with the rounding approach. Good.

Let me write snap:
```
function snapQuat(q){
  const r = [0, 1, -1];
  const v = 1/Math.SQRT2;
  const set = [0, v, -v, 1, -1];
  // round each component
  const snap = a => set.reduce((p,c)=> Math.abs(a-c)<Math.abs(a-p)?c:p, 0);
  q.x = snap(q.x); q.y=snap(q.y); q.z=snap(q.z); q.w=snap(q.w);
  q.normalize();
}
```

Good.

Now the move function with tween and queue. Let me write a simple animation manager:

```
let animating = false;
const moveQueue = [];
function enqueue(move){ moveQueue.push(move); processQueue(); }
function processQueue(){ if(!animating && moveQueue.length) startMove(moveQueue.shift()); }
function startMove(move){ animating=true; ... pivot setup ... tween }
```

move = {axis, layer, dir, duration}.

Tween via clock. Let me use performance.now.

I'll write startMove to set up pivot, store start time, and in the render loop handle the in-flight move.

Let me code the full file now.

Scramble: push ~20 random moves with short duration. Disable interactions during scramble (they're queued anyway). I'll just enqueue many moves; the button enqueues a fresh scramble.

Let me also add a "Reset" maybe. The requirement only asks scramble. I'll add scramble button plus instructions. Maybe add reset too for nicety. Keep focused: scramble button, orbit drag, clickable faces. I'll add a Reset button as bonus and a move counter.

Let me write it.

For picking the sticker normal: I'll store on each sticker mesh userData = {axis:'x', sign:1, cubie: groupRef}. Actually the outward normal of the sticker. Let me compute from face when building.

Let me build stickers with normals:
- +X face: normal (1,0,0), sticker at x=0.48, rotation facing +X.
- -X: (-1,0,0) at x=-0.48
- +Y: (0,1,0) at y=0.48
- -Y: (0,-1,0) at y=-0.48
- +Z: (0,0,1) at z=0.48
- -Z: (0,0,-1) at z=-0.48

Sticker plane default faces +Z. For +Z sticker no rotation needed. For +X, rotateY 90°. Etc.

I'll create plane and set position and lookAt outward or use rotation.

Simplest: PlaneGeometry, set rotation:
- +Z: rot (0,0,0)
- -Z: rot (0, Math.PI, 0)
- +X: rot (0, Math.PI/2, 0)
- -X: rot (0, -Math.PI/2, 0)
- +Y: rot (-Math.PI/2, 0, 0)
- -Y: rot (Math.PI/2, 0, 0)

Position = normal * 0.475 (box half size 0.475 if box is 0.95). Let me use box size 0.92, stickers at 0.461, sticker size 0.78.

Store userData.normal = the axis vector, and the axis name & sign.

On click raycast, intersects[0].object is the sticker. Get normal = userData.normal. axis = which component is nonzero. layer = cubie.position on that axis rounded (should be ±1). dir = decide.

Let me determine dir to make clockwise-from-outside:
For outward normal n (unit along one axis), rotation is about axis n. The angle: clockwise when viewed from outside (from +n side looking toward -n). A positive right-hand rotation about +n appears counterclockwise when viewed from +n side. So clockwise from outside = negative rotation about n. So dir = -1 (angle = -90° about the axis n). So I'll set angle = -Math.PI/2 about the axis vector n. Equivalent: rotateSlice(axis, sign, -1) meaning dir=-1 -> angle = dir*90° about +axis... but if sign is -1, the outward normal is -axis, so the rotation axis for clockwise-from-outside is -axis, angle = -90° → total = rotate about -axis by -90° = about +axis by +90°. 

Let me just compute the angle about the +axis: angle = -sign * Math.PI/2 * ... let me think. Outward normal n = sign * (unit axis). Clockwise from outside = -90° about n = -90° about (sign*unitAxis) = -sign*90° about +unitAxis. So angle about +axis = -sign * (Math.PI/2). So dir relative to +axis = -sign.

So rotateSlice(axis, layer=sign, dir=-sign) effectively angle = dir * 90° about +axis = -sign*90°. 

I'll implement rotateSlice(axis, layer, angleSign) where angleSign multiplies Math.PI/2 about +axis.

So for click: angleSign = -sign. layer = sign.

Good.

Let me write everything.

Render loop: rotate any active pivot, update orbit camera, render.

Let me code now.

I'll build the complete interactive Rubik's Cube app in a single index.html.

write /workspace/index.html (12171 bytes)
toolResult

Successfully wrote 12171 bytes to /workspace/index.html

assistant
thinking
Let me verify it loads without errors by checking the file is valid HTML and three.min.js exposes THREE global. Quick sanity check that three.min.js has THREE.
bash {"command":"head -c 300 three.min.js | grep -o \"THREE\" | head -1; echo \"---\"; ls -la index.html"}
toolResult

THREE --- -rw-r--r-- 1 node node 12180 Jul 10 00:33 index.html

assistant

Done. I built the entire app into `/workspace/index.html` — a single self-contained file loading the local `three.min.js`. ## What it does **Interactive 3D Rubik's Cube** — a real 27-cubie model (not just textured faces), so every rotation is a genuine slice transform. ### Features - **Click-to-rotate faces** — Raycasts against the colored sticker meshes. Clicking any outer sticker rotates that face 90°. The rotation direction is computed from the outward normal so the clicked face turns clockwise from the viewer's perspective. A drag-vs-click threshold (6px) cleanly separates orbit drags from face clicks. - **Orbit-drag camera** — Custom spherical orbit controls (no `OrbitControls` dependency, which isn't in `three.min.js`). Drag to orbit, scroll to zoom, with phi clamped to avoid gimbal flip. - **Scramble button** — Enqueues 22 non-redundant random moves (axis/layer/dir) that animate sequentially at a fast speed. The button disables itself while a scramble is running. - **Reset button** — Rebuilds a solved cube instantly and cancels any queued moves. ### Implementation notes - Each cubie is a `Group` with a black `BoxGeometry` body plus inset colored `PlaneGeometry` stickers on outward faces — giving the classic black-gap look. - Slice rotations use a temporary `pivot` group with `pivot.attach(...)` / `cubeRoot.attach(...)` to reparent cubies in world space, tweened with eased `requestAnimationFrame`. - After each move, cubie positions are integer-rounded and quaternions are snapped to the nearest valid 90°-multiple set `{-1, -√½, 0, √½, 1}` then normalized — preventing floating-point drift across long scramble chains. - A move queue drives both clicks and scrambles so animations never collide. - Lighting uses ambient + key/fill directional lights for soft depth. Open `index.html` in a browser to use it.

TAKE IT TO CODE

A single self-contained HTML file — copy it and open in any browser, or reuse the prompt with your own pi-agent run.

RUN

modelglm-5.2
vendorZ.ai · GLM
harnesspi-agent
generated2026-07-10

EVALUATION

4/5 working

3D Rubik's Cube renders correctly, face rotation on click, and drag/wheel orbit camera all work; keyboard has no effect as expected.

Vision grade · anthropic/claude-sonnet-5

MEASUREMENTS

Raw readings from the capture, not a verdict — the grade above is the only judgement. Percentages are how much of the screen changed; the measure under-reports sparse motion on a dark background.

idle motion0.0%
on click3.4%
on drag16.2%
on wheel40.3%
on enter + space0.0%
on arrow keys0.0%
on w/a/s/d0.0%
frame spread49.9 / 255
console errors0
js errors none

METRICS

runtime129.7s
tokens in2.5k
tokens out7.9k
cached19k
cost / run$0.0293