← Back to projects

Flutter package

Puzzle Cube

An interactive 3D twisty-puzzle cube for Flutter. It bundles a pure-Dart 3x3 cube model (layer turns, scrambling, validation, JSON, solved-state detection) with a gesture-driven widget that renders the cube in 3D and lets you orbit it, tap its faces, and turn layers by dragging them with your finger.

View on pub.dev →
Flutter Dart 3D rendering MIT

The 3D rendering is built on ditredi.

Features

Getting started

dependencies:
  puzzle_cube: ^0.1.0
import 'package:puzzle_cube/puzzle_cube.dart';

Usage

Interactive widget

class MyCube extends StatefulWidget {
  const MyCube({super.key});
  @override
  State<MyCube> createState() => _MyCubeState();
}

class _MyCubeState extends State<MyCube> {
  final controller = CubeController(
    moveDuration: const Duration(milliseconds: 350),
    initialViewRotationX: -0.5,
    initialViewRotationY: 0.6,
  );

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 300,
      height: 300,
      child: Cube(
        controller: controller,
        // Enable drag-to-turn. Dragging off the cube orbits the view.
        // Commit the turn to the cube state (the gesture already showed it).
        onMove: (move) {
          controller.applyMoveInstant(move);
          debugPrint('turned $move');
        },
        // Enable tap-to-select. Reports the front-most face under the tap.
        onFaceTap: (face) => debugPrint('tapped $face'),
      ),
    );
  }
}
Animated demo of the Puzzle Cube widget being turned and orbited

Buttons can drive the same controller:

ElevatedButton(onPressed: () => controller.play(CubeMove.r), child: const Text('R'));
ElevatedButton(onPressed: () => controller.play(CubeMove.r.inverse), child: const Text("R'"));
ElevatedButton(onPressed: () => controller.scramble(), child: const Text('Scramble'));
ElevatedButton(onPressed: controller.reset, child: const Text('Reset'));
ElevatedButton(onPressed: controller.resetCamera, child: const Text('Recenter'));

Apply an algorithm as an animated sequence:

controller.playSequence(const [
  CubeMove.r, CubeMove.u, CubeMove.ri, CubeMove.ui,
]);

Render a static, non-interactive cube:

Cube(controller: controller, enableGestures: false);

Driving the controller

final controller = CubeController();

controller.play(CubeMove.r);                 // queue one animated turn
controller.applyMoveInstant(CubeMove.r);     // apply with no animation (commit a drag)
controller.playSequence(const [CubeMove.u]); // queue several
controller.scramble(moves: 25, seed: 42);    // reproducible scramble
controller.reset();                          // back to a solved cube
controller.resetCamera();                    // restore the initial view
controller.rotateView(dx: 0.1, dy: -0.05);   // orbit programmatically

controller.isSolved;      // solved AND idle (nothing queued/animating)
controller.isAnimating;   // a move is mid-animation
controller.hasQueuedWork; // a move is animating or queued
controller.pendingMove;   // the move currently animating, or null
controller.state;         // the underlying PuzzleCubeState

Pure model (no widget)

final cube = PuzzleCubeState.solved();
cube.applyMove(CubeMove.r);
cube.applyMove(CubeMove.u);
cube.applyMove(CubeMove.r.inverse); // R'
print(cube.isSolved); // false

final scrambled = PuzzleCubeState.random(moves: 25, seed: 42);
final json = scrambled.toJson();
final restored = PuzzleCubeState.fromJson(json);

final cubie = cube.cubieAt(1, 1, 1); // the piece at a grid position

Paint a cube ("colour my cube")

// Centres are fixed; every other sticker starts blank.
final controller = CubeController(initialState: PuzzleCubeState.colorless());

controller.setStickerColor(
  x: 1, y: 1, z: 1,
  face: CubieFace.xPos,
  color: CubeColors.green,
); // ignored on centres and while a move is animating

// Or replace the whole state at once.
controller.replaceState(PuzzleCubeState.solved());

Validate a colouring

const validator = CubeColorValidator();
final result = validator.validate(controller.state);
if (!result.isValid) {
  for (final issue in result.issues) {
    debugPrint(issue.message); // e.g. "Green appears 8 times. Expected 9."
  }
}

API reference

Class / memberPurpose
PuzzleCubeState.solved()A solved cube.
PuzzleCubeState.colorless()Centres fixed, every other sticker blank.
PuzzleCubeState.random({moves, seed})Reproducible scramble.
applyMove(CubeMove)Apply one quarter-turn in place.
CubeController.applyMoveInstant(CubeMove)Commit a move with no animation (used in onMove).
cubieAt(x, y, z)The cubie at a grid position, or null.
setStickerColor({x, y, z, face, color})Paint a sticker (centres are fixed).
isSolved, copy(), toJson()/fromJson()State helpers.
CubeMove / .inverse12 outer + 6 slice moves, with inverses.
CubieModel, CubieFaceA single cubie and its six faces.
CubeColorsSix colours, palette, nearest, nameOf, areOpposites.
CubeControllerDrives the widget: queue moves, orbit, scramble, paint.
CubeThe interactive 3D widget (onMove, onFaceTap, enableGestures).
CubeColorValidatorReal-cube colour validation.
CubeValidationResult, CubeValidationIssueValidation output.
cubeFaceAtTap, cubeDragFor, CubeDragLow-level projection helpers.
CubieBuilderBuilds the DiTreDi geometry for a cubie.

Example app

A runnable demo lives in example/:

cd example
flutter run

License

MIT. See LICENSE.

Developers

The Absolute Devs team behind Puzzle Cube. Meet the full team →