Rubik's Cube

This is an implementation of an rubik's cube.

The whole code can be found

here

The Rubik's cubes rotation currently doesn't always behave as expected.
There is an [issue](https://github.com/Myrmod/svelte-babylon/issues/24) for fixing this misbehaviour.
The Rubik's cubes rotation currently doesn't always behave as expected.
There is an [issue](https://github.com/Myrmod/svelte-babylon/issues/24) for fixing this misbehaviour.
null

Example

Click to see example!

Example Code

<script lang="ts">
	import type {
		AbstractMesh,
		Engine,
		InstancedMesh,
		TransformNode as BTransformNode,
	} from '@babylonjs/core'
	import { Animation } from '@babylonjs/core/Animations/animation.js'
	// this import is required for the rotation to work
	import '@babylonjs/core/Behaviors/Meshes/pointerDragBehavior.js'
	import type { ArcRotateCamera as ACamera } from '@babylonjs/core/Cameras/arcRotateCamera'
	import { PointerEventTypes } from '@babylonjs/core/Events/pointerEvents.js'
	import { Color3 } from '@babylonjs/core/Maths/math.color.js'
	import { Vector3 } from '@babylonjs/core/Maths/math.vector.js'
	import type { Scene as BScene } from '@babylonjs/core/scene.js'
	import ArcRotateCamera from 'svelte-babylon/components/Cameras/ArcRotateCamera/index.svelte'
	import Canvas from 'svelte-babylon/components/Canvas/index.svelte'
	import HemisphericLight from 'svelte-babylon/components/Lights/HemisphericLight/index.svelte'
	import StandardMaterial from 'svelte-babylon/components/Materials/StandardMaterial/index.svelte'
	import Instance from 'svelte-babylon/components/Misc/Instance/index.svelte'
	import Box from 'svelte-babylon/components/Objects/Box/index.svelte'
	import Plane from 'svelte-babylon/components/Objects/Plane/index.svelte'
	import Scene from 'svelte-babylon/components/Scene/index.svelte'
	import TransformNode from 'svelte-babylon/components/TransformNode/index.svelte'
	import degreeToRadians from 'svelte-babylon/utils/Math/degreeToRadians'
	import type { Writable } from 'svelte/types/runtime/store'

	let width = 3
	let scene: Writable<BScene>
	let transformNode: Writable<BTransformNode>
	let camera: Writable<ACamera>
	let engine: Writable<Engine>
	let cubeWidth = 1
	let centerOfCube = Vector3.Zero()

	const planeDetailsArray = [
		{
			color: new Color3(1, 1, 1), // white
			rotation: new Vector3(Math.PI / 2, 0, 0),
			normal: new Vector3(0, 1, 0),
		},
		{
			color: new Color3(1, 0, 0), // red
			rotation: new Vector3(-Math.PI / 2, 0, 0),
			normal: new Vector3(0, -1, 0),
		},
		{
			color: new Color3(0, 1, 0), // green
			rotation: new Vector3(0, -Math.PI / 2, 0),
			normal: new Vector3(1, 0, 0),
		},
		{
			color: new Color3(0, 0, 1), // blue
			rotation: new Vector3(0, Math.PI / 2, 0),
			normal: new Vector3(-1, 0, 0),
		},
		{
			color: new Color3(1, 120 / 255, 180 / 255), // rosé
			rotation: new Vector3(Math.PI, 0, 0),
			normal: new Vector3(0, 0, 1),
		},
		{
			color: new Color3(200 / 255 / 255, 200 / 255, 1), // cyan
			rotation: new Vector3(0, 0, 0),
			normal: new Vector3(0, 0, -1),
		},
	]

	let cubes: Array<InstancedMesh> = []
	function handleBoxInstanceCreation(instance: InstancedMesh, _index: number) {
		if (cubes.length === Math.pow(width, 3)) {
			return
		}

		cubes = [...cubes, instance]
	}

	function handlePlaneInstanceCreation(
		instance: InstancedMesh,
		index: number,
		planeDetails: (typeof planeDetailsArray)[0],
		cube: InstancedMesh,
	) {
		instance.isPickable = true
		instance.parent = cube
		instance.name = `${cube.name}-Plane-${index}`
		instance.position = planeDetails.normal.scale(0.501).clone()
		instance.rotation = planeDetails.rotation.clone()
		instance.renderOutline = true
	}

	// position our instances
	$: if (cubes.length === Math.pow(width, 3)) {
		let counter = 0
		for (let x = 0; x < width; x++) {
			for (let y = 0; y < width; y++) {
				for (let z = 0; z < width; z++) {
					cubes[counter].position = new Vector3(x, y, z)
					cubes[counter].setParent(null)
					counter++
				}
			}
		}

		centerOfCube = cubes
			.reduce((total, cube) => total.addInPlace(cube.position), Vector3.Zero())
			.scale(1 / cubes.length)
	}

	let eventAdded = false
	let firstPick: AbstractMesh = undefined
	let startPoint = {
		x: 0,
		y: 0,
	}
	let endPoint = {
		x: 0,
		y: 0,
	}
	$: if ($scene && !eventAdded) {
		eventAdded = true
		$scene.onPointerObservable.add(eventData => {
			switch (eventData.type) {
				case PointerEventTypes.POINTERDOWN:
					startPoint.x = eventData.event.clientX
					startPoint.y = eventData.event.clientY

					firstPick = $scene.pick($scene.pointerX, $scene.pointerY).pickedMesh
					if (firstPick) $camera.detachControl()
					break

				case PointerEventTypes.POINTERUP:
					if (!firstPick?.parent) return

					endPoint.x = eventData.event.clientX
					endPoint.y = eventData.event.clientY

					if (
						Math.abs(startPoint.x - endPoint.x) <= 10 &&
						Math.abs(startPoint.y - endPoint.y) <= 10
					)
						return

					const movementVector = getMovementVector(startPoint, endPoint)

					const cubesToRotate = getCubesToRotate(firstPick.parent as AbstractMesh, movementVector)

					// set temporary parent
					cubesToRotate?.forEach(cube => {
						cube.setParent($transformNode)
					})

					const from = movementVector.x ? $transformNode.rotation.y : $transformNode.rotation.x
					const to = from + degreeToRadians(90 * (movementVector.x * -1 || movementVector.y))
					Animation.CreateAndStartAnimation(
						'rotate',
						$transformNode,
						movementVector.x ? 'rotation.y' : 'rotation.x',
						60,
						15,
						from,
						to,
						0,
					).onAnimationEnd = () => {
						cubesToRotate?.forEach(cube => {
							cube.setParent(null)
						})

						firstPick = null
						$camera.attachControl()
					}

					break
				default:
					break
			}
		})
	}

	function getMovementVector(
		start: { x: number; y: number },
		end: { x: number; y: number },
	): Vector3 {
		if (Math.abs(start.y - end.y) > Math.abs(start.x - end.x)) {
			return start.y - end.y < 0 ? Vector3.Down() : Vector3.Up()
		}

		return start.x - end.x > 0 ? Vector3.Left() : Vector3.Right()
	}

	function getCubesToRotate(base: AbstractMesh, direction: Vector3) {
		try {
			if (direction.x) {
				return cubes.filter(cube => cube.position.y === base.position.y)
			}

			return cubes.filter(cube => cube.position.x === base.position.x)
		} catch (error) {}
	}

	function findCubesByPlaneDetails(planeDetails: (typeof planeDetailsArray)[0]) {
		return cubes.filter(
			cube => !cubes.some(ocube => ocube.position.equals(cube.position.add(planeDetails.normal))),
		)
	}
</script>

<Canvas
	antialiasing={true}
	engineOptions={{
		preserveDrawingBuffer: true,
		stencil: true,
	}}
	bind:engine
>
	<Scene bind:scene clearColor={Color3.White()} animationsEnabled>
		<HemisphericLight />
		<ArcRotateCamera bind:camera radius={10} target={centerOfCube} alpha={Math.PI / 2} />
		<TransformNode bind:object={transformNode} position={centerOfCube}>
			{#if cubes.length === Math.pow(width, 3)}
				{#each planeDetailsArray as planeDetails}
					<Plane options={{ size: 0.96 }}>
						<StandardMaterial diffuseColor={planeDetails.color} specularColor={Color3.Black()} />
						{#each findCubesByPlaneDetails(planeDetails) as cube}
							<Instance
								onCreated={(instance, index) =>
									handlePlaneInstanceCreation(instance, index, planeDetails, cube)}
							/>
						{/each}
					</Plane>
				{/each}
			{/if}
			<!-- we position the box far away (out of rendering range) to hide it, this way we don't -->
			<Box position={new Vector3(-9999, -9999, -9999)} options={{ size: cubeWidth }}>
				<StandardMaterial diffuseColor={Color3.Black()} specularColor={Color3.Black()} />
				<Instance number={Math.pow(width, 3)} onCreated={handleBoxInstanceCreation} />
			</Box>
		</TransformNode>
	</Scene>
</Canvas>
<script lang="ts">
	import type {
		AbstractMesh,
		Engine,
		InstancedMesh,
		TransformNode as BTransformNode,
	} from '@babylonjs/core'
	import { Animation } from '@babylonjs/core/Animations/animation.js'
	// this import is required for the rotation to work
	import '@babylonjs/core/Behaviors/Meshes/pointerDragBehavior.js'
	import type { ArcRotateCamera as ACamera } from '@babylonjs/core/Cameras/arcRotateCamera'
	import { PointerEventTypes } from '@babylonjs/core/Events/pointerEvents.js'
	import { Color3 } from '@babylonjs/core/Maths/math.color.js'
	import { Vector3 } from '@babylonjs/core/Maths/math.vector.js'
	import type { Scene as BScene } from '@babylonjs/core/scene.js'
	import ArcRotateCamera from 'svelte-babylon/components/Cameras/ArcRotateCamera/index.svelte'
	import Canvas from 'svelte-babylon/components/Canvas/index.svelte'
	import HemisphericLight from 'svelte-babylon/components/Lights/HemisphericLight/index.svelte'
	import StandardMaterial from 'svelte-babylon/components/Materials/StandardMaterial/index.svelte'
	import Instance from 'svelte-babylon/components/Misc/Instance/index.svelte'
	import Box from 'svelte-babylon/components/Objects/Box/index.svelte'
	import Plane from 'svelte-babylon/components/Objects/Plane/index.svelte'
	import Scene from 'svelte-babylon/components/Scene/index.svelte'
	import TransformNode from 'svelte-babylon/components/TransformNode/index.svelte'
	import degreeToRadians from 'svelte-babylon/utils/Math/degreeToRadians'
	import type { Writable } from 'svelte/types/runtime/store'

	let width = 3
	let scene: Writable<BScene>
	let transformNode: Writable<BTransformNode>
	let camera: Writable<ACamera>
	let engine: Writable<Engine>
	let cubeWidth = 1
	let centerOfCube = Vector3.Zero()

	const planeDetailsArray = [
		{
			color: new Color3(1, 1, 1), // white
			rotation: new Vector3(Math.PI / 2, 0, 0),
			normal: new Vector3(0, 1, 0),
		},
		{
			color: new Color3(1, 0, 0), // red
			rotation: new Vector3(-Math.PI / 2, 0, 0),
			normal: new Vector3(0, -1, 0),
		},
		{
			color: new Color3(0, 1, 0), // green
			rotation: new Vector3(0, -Math.PI / 2, 0),
			normal: new Vector3(1, 0, 0),
		},
		{
			color: new Color3(0, 0, 1), // blue
			rotation: new Vector3(0, Math.PI / 2, 0),
			normal: new Vector3(-1, 0, 0),
		},
		{
			color: new Color3(1, 120 / 255, 180 / 255), // rosé
			rotation: new Vector3(Math.PI, 0, 0),
			normal: new Vector3(0, 0, 1),
		},
		{
			color: new Color3(200 / 255 / 255, 200 / 255, 1), // cyan
			rotation: new Vector3(0, 0, 0),
			normal: new Vector3(0, 0, -1),
		},
	]

	let cubes: Array<InstancedMesh> = []
	function handleBoxInstanceCreation(instance: InstancedMesh, _index: number) {
		if (cubes.length === Math.pow(width, 3)) {
			return
		}

		cubes = [...cubes, instance]
	}

	function handlePlaneInstanceCreation(
		instance: InstancedMesh,
		index: number,
		planeDetails: (typeof planeDetailsArray)[0],
		cube: InstancedMesh,
	) {
		instance.isPickable = true
		instance.parent = cube
		instance.name = `${cube.name}-Plane-${index}`
		instance.position = planeDetails.normal.scale(0.501).clone()
		instance.rotation = planeDetails.rotation.clone()
		instance.renderOutline = true
	}

	// position our instances
	$: if (cubes.length === Math.pow(width, 3)) {
		let counter = 0
		for (let x = 0; x < width; x++) {
			for (let y = 0; y < width; y++) {
				for (let z = 0; z < width; z++) {
					cubes[counter].position = new Vector3(x, y, z)
					cubes[counter].setParent(null)
					counter++
				}
			}
		}

		centerOfCube = cubes
			.reduce((total, cube) => total.addInPlace(cube.position), Vector3.Zero())
			.scale(1 / cubes.length)
	}

	let eventAdded = false
	let firstPick: AbstractMesh = undefined
	let startPoint = {
		x: 0,
		y: 0,
	}
	let endPoint = {
		x: 0,
		y: 0,
	}
	$: if ($scene && !eventAdded) {
		eventAdded = true
		$scene.onPointerObservable.add(eventData => {
			switch (eventData.type) {
				case PointerEventTypes.POINTERDOWN:
					startPoint.x = eventData.event.clientX
					startPoint.y = eventData.event.clientY

					firstPick = $scene.pick($scene.pointerX, $scene.pointerY).pickedMesh
					if (firstPick) $camera.detachControl()
					break

				case PointerEventTypes.POINTERUP:
					if (!firstPick?.parent) return

					endPoint.x = eventData.event.clientX
					endPoint.y = eventData.event.clientY

					if (
						Math.abs(startPoint.x - endPoint.x) <= 10 &&
						Math.abs(startPoint.y - endPoint.y) <= 10
					)
						return

					const movementVector = getMovementVector(startPoint, endPoint)

					const cubesToRotate = getCubesToRotate(firstPick.parent as AbstractMesh, movementVector)

					// set temporary parent
					cubesToRotate?.forEach(cube => {
						cube.setParent($transformNode)
					})

					const from = movementVector.x ? $transformNode.rotation.y : $transformNode.rotation.x
					const to = from + degreeToRadians(90 * (movementVector.x * -1 || movementVector.y))
					Animation.CreateAndStartAnimation(
						'rotate',
						$transformNode,
						movementVector.x ? 'rotation.y' : 'rotation.x',
						60,
						15,
						from,
						to,
						0,
					).onAnimationEnd = () => {
						cubesToRotate?.forEach(cube => {
							cube.setParent(null)
						})

						firstPick = null
						$camera.attachControl()
					}

					break
				default:
					break
			}
		})
	}

	function getMovementVector(
		start: { x: number; y: number },
		end: { x: number; y: number },
	): Vector3 {
		if (Math.abs(start.y - end.y) > Math.abs(start.x - end.x)) {
			return start.y - end.y < 0 ? Vector3.Down() : Vector3.Up()
		}

		return start.x - end.x > 0 ? Vector3.Left() : Vector3.Right()
	}

	function getCubesToRotate(base: AbstractMesh, direction: Vector3) {
		try {
			if (direction.x) {
				return cubes.filter(cube => cube.position.y === base.position.y)
			}

			return cubes.filter(cube => cube.position.x === base.position.x)
		} catch (error) {}
	}

	function findCubesByPlaneDetails(planeDetails: (typeof planeDetailsArray)[0]) {
		return cubes.filter(
			cube => !cubes.some(ocube => ocube.position.equals(cube.position.add(planeDetails.normal))),
		)
	}
</script>

<Canvas
	antialiasing={true}
	engineOptions={{
		preserveDrawingBuffer: true,
		stencil: true,
	}}
	bind:engine
>
	<Scene bind:scene clearColor={Color3.White()} animationsEnabled>
		<HemisphericLight />
		<ArcRotateCamera bind:camera radius={10} target={centerOfCube} alpha={Math.PI / 2} />
		<TransformNode bind:object={transformNode} position={centerOfCube}>
			{#if cubes.length === Math.pow(width, 3)}
				{#each planeDetailsArray as planeDetails}
					<Plane options={{ size: 0.96 }}>
						<StandardMaterial diffuseColor={planeDetails.color} specularColor={Color3.Black()} />
						{#each findCubesByPlaneDetails(planeDetails) as cube}
							<Instance
								onCreated={(instance, index) =>
									handlePlaneInstanceCreation(instance, index, planeDetails, cube)}
							/>
						{/each}
					</Plane>
				{/each}
			{/if}
			<!-- we position the box far away (out of rendering range) to hide it, this way we don't -->
			<Box position={new Vector3(-9999, -9999, -9999)} options={{ size: cubeWidth }}>
				<StandardMaterial diffuseColor={Color3.Black()} specularColor={Color3.Black()} />
				<Instance number={Math.pow(width, 3)} onCreated={handleBoxInstanceCreation} />
			</Box>
		</TransformNode>
	</Scene>
</Canvas>
svelte