Spaces:
Running
Running
format codes
Browse files- .prettierrc +0 -1
- .prettierrc.js +11 -0
- bun.lock +27 -0
- eslint.config.mjs +5 -11
- next.config.ts +1 -1
- package.json +1 -0
- postcss.config.mjs +1 -1
- public/file.svg +0 -1
- public/globe.svg +0 -1
- public/next.svg +0 -1
- public/vercel.svg +0 -1
- public/window.svg +0 -1
- src/app/globals.css +1 -1
- src/app/layout.tsx +4 -4
- src/app/page.tsx +2 -4
- src/components/canvas.tsx +12 -19
- src/components/consts.tsx +14 -20
- src/components/cube-piece.tsx +16 -27
- src/components/env.tsx +5 -4
- src/components/rotation-controller.ts +29 -27
- src/components/rotation-panel.tsx +12 -27
- src/components/rotator.tsx +36 -40
- src/components/rubiks-cube.tsx +32 -38
.prettierrc
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{}
|
|
|
|
|
|
.prettierrc.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
printWidth: 120,
|
| 3 |
+
tabWidth: 2,
|
| 4 |
+
trailingComma: 'all',
|
| 5 |
+
singleQuote: true,
|
| 6 |
+
semi: true,
|
| 7 |
+
importOrder: ['^@/(.*)$', '^[./]'],
|
| 8 |
+
importOrderSeparation: true,
|
| 9 |
+
importOrderSortSpecifiers: true,
|
| 10 |
+
plugins: ['@trivago/prettier-plugin-sort-imports'],
|
| 11 |
+
};
|
bun.lock
CHANGED
|
@@ -14,6 +14,7 @@
|
|
| 14 |
"devDependencies": {
|
| 15 |
"@eslint/eslintrc": "^3",
|
| 16 |
"@tailwindcss/postcss": "^4",
|
|
|
|
| 17 |
"@types/node": "^20",
|
| 18 |
"@types/react": "^19",
|
| 19 |
"@types/react-dom": "^19",
|
|
@@ -28,8 +29,26 @@
|
|
| 28 |
"packages": {
|
| 29 |
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
"@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="],
|
| 34 |
|
| 35 |
"@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
|
|
@@ -242,6 +261,8 @@
|
|
| 242 |
|
| 243 |
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.13", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "postcss": "^8.4.41", "tailwindcss": "4.1.13" } }, "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ=="],
|
| 244 |
|
|
|
|
|
|
|
| 245 |
"@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
|
| 246 |
|
| 247 |
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
|
@@ -666,12 +687,16 @@
|
|
| 666 |
|
| 667 |
"its-fine": ["its-fine@2.0.0", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng=="],
|
| 668 |
|
|
|
|
|
|
|
| 669 |
"jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
|
| 670 |
|
| 671 |
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
| 672 |
|
| 673 |
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
| 674 |
|
|
|
|
|
|
|
| 675 |
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
| 676 |
|
| 677 |
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
|
@@ -718,6 +743,8 @@
|
|
| 718 |
|
| 719 |
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
| 720 |
|
|
|
|
|
|
|
| 721 |
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
| 722 |
|
| 723 |
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
|
|
|
| 14 |
"devDependencies": {
|
| 15 |
"@eslint/eslintrc": "^3",
|
| 16 |
"@tailwindcss/postcss": "^4",
|
| 17 |
+
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
| 18 |
"@types/node": "^20",
|
| 19 |
"@types/react": "^19",
|
| 20 |
"@types/react-dom": "^19",
|
|
|
|
| 29 |
"packages": {
|
| 30 |
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
| 31 |
|
| 32 |
+
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
| 33 |
+
|
| 34 |
+
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
|
| 35 |
+
|
| 36 |
+
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
| 37 |
+
|
| 38 |
+
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
| 39 |
+
|
| 40 |
+
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
| 41 |
+
|
| 42 |
+
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
|
| 43 |
+
|
| 44 |
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
| 45 |
|
| 46 |
+
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
| 47 |
+
|
| 48 |
+
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
|
| 49 |
+
|
| 50 |
+
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
|
| 51 |
+
|
| 52 |
"@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="],
|
| 53 |
|
| 54 |
"@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
|
|
|
|
| 261 |
|
| 262 |
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.13", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "postcss": "^8.4.41", "tailwindcss": "4.1.13" } }, "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ=="],
|
| 263 |
|
| 264 |
+
"@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@5.2.2", "", { "dependencies": { "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-svelte", "svelte"] }, "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA=="],
|
| 265 |
+
|
| 266 |
"@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="],
|
| 267 |
|
| 268 |
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
|
|
|
| 687 |
|
| 688 |
"its-fine": ["its-fine@2.0.0", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng=="],
|
| 689 |
|
| 690 |
+
"javascript-natural-sort": ["javascript-natural-sort@0.7.1", "", {}, "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="],
|
| 691 |
+
|
| 692 |
"jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
|
| 693 |
|
| 694 |
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
| 695 |
|
| 696 |
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
| 697 |
|
| 698 |
+
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
| 699 |
+
|
| 700 |
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
| 701 |
|
| 702 |
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
|
|
|
| 743 |
|
| 744 |
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
| 745 |
|
| 746 |
+
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
| 747 |
+
|
| 748 |
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
| 749 |
|
| 750 |
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
eslint.config.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import {
|
| 3 |
-
import {
|
| 4 |
|
| 5 |
const __filename = fileURLToPath(import.meta.url);
|
| 6 |
const __dirname = dirname(__filename);
|
|
@@ -10,15 +10,9 @@ const compat = new FlatCompat({
|
|
| 10 |
});
|
| 11 |
|
| 12 |
const eslintConfig = [
|
| 13 |
-
...compat.extends(
|
| 14 |
{
|
| 15 |
-
ignores: [
|
| 16 |
-
"node_modules/**",
|
| 17 |
-
".next/**",
|
| 18 |
-
"out/**",
|
| 19 |
-
"build/**",
|
| 20 |
-
"next-env.d.ts",
|
| 21 |
-
],
|
| 22 |
},
|
| 23 |
];
|
| 24 |
|
|
|
|
| 1 |
+
import { FlatCompat } from '@eslint/eslintrc';
|
| 2 |
+
import { dirname } from 'path';
|
| 3 |
+
import { fileURLToPath } from 'url';
|
| 4 |
|
| 5 |
const __filename = fileURLToPath(import.meta.url);
|
| 6 |
const __dirname = dirname(__filename);
|
|
|
|
| 10 |
});
|
| 11 |
|
| 12 |
const eslintConfig = [
|
| 13 |
+
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
| 14 |
{
|
| 15 |
+
ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
},
|
| 17 |
];
|
| 18 |
|
next.config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import type { NextConfig } from
|
| 2 |
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
/* config options here */
|
|
|
|
| 1 |
+
import type { NextConfig } from 'next';
|
| 2 |
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
/* config options here */
|
package.json
CHANGED
|
@@ -20,6 +20,7 @@
|
|
| 20 |
"devDependencies": {
|
| 21 |
"@eslint/eslintrc": "^3",
|
| 22 |
"@tailwindcss/postcss": "^4",
|
|
|
|
| 23 |
"@types/node": "^20",
|
| 24 |
"@types/react": "^19",
|
| 25 |
"@types/react-dom": "^19",
|
|
|
|
| 20 |
"devDependencies": {
|
| 21 |
"@eslint/eslintrc": "^3",
|
| 22 |
"@tailwindcss/postcss": "^4",
|
| 23 |
+
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
| 24 |
"@types/node": "^20",
|
| 25 |
"@types/react": "^19",
|
| 26 |
"@types/react-dom": "^19",
|
postcss.config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
const config = {
|
| 2 |
-
plugins: [
|
| 3 |
};
|
| 4 |
|
| 5 |
export default config;
|
|
|
|
| 1 |
const config = {
|
| 2 |
+
plugins: ['@tailwindcss/postcss'],
|
| 3 |
};
|
| 4 |
|
| 5 |
export default config;
|
public/file.svg
DELETED
public/globe.svg
DELETED
public/next.svg
DELETED
public/vercel.svg
DELETED
public/window.svg
DELETED
src/app/globals.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
@import
|
| 2 |
|
| 3 |
::-webkit-scrollbar {
|
| 4 |
display: none;
|
|
|
|
| 1 |
+
@import 'tailwindcss';
|
| 2 |
|
| 3 |
::-webkit-scrollbar {
|
| 4 |
display: none;
|
src/app/layout.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
import type { Metadata } from
|
| 2 |
-
|
|
|
|
| 3 |
|
| 4 |
export const metadata: Metadata = {
|
| 5 |
title: "Rubik's Cube Solver - with Reinforcement Learning",
|
| 6 |
-
description:
|
| 7 |
-
"A web application for solving Rubik's Cube using Reinforcement Learning techniques.",
|
| 8 |
};
|
| 9 |
|
| 10 |
export default function RootLayout({
|
|
|
|
| 1 |
+
import type { Metadata } from 'next';
|
| 2 |
+
|
| 3 |
+
import './globals.css';
|
| 4 |
|
| 5 |
export const metadata: Metadata = {
|
| 6 |
title: "Rubik's Cube Solver - with Reinforcement Learning",
|
| 7 |
+
description: "A web application for solving Rubik's Cube using Reinforcement Learning techniques.",
|
|
|
|
| 8 |
};
|
| 9 |
|
| 10 |
export default function RootLayout({
|
src/app/page.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { Canvas } from
|
| 2 |
|
| 3 |
export default function Home() {
|
| 4 |
return (
|
|
@@ -6,9 +6,7 @@ export default function Home() {
|
|
| 6 |
<div className="absolute inset-0 z-10 pointer-events-none">
|
| 7 |
<div className="w-full h-full p-6">
|
| 8 |
<div className="text-6xl font-bold">Rubik's Cube Solver</div>
|
| 9 |
-
<div className="text-2xl text-gray-800">
|
| 10 |
-
with Reinforcement Learning
|
| 11 |
-
</div>
|
| 12 |
<div className="text-gray-700">
|
| 13 |
<a href="https://cross-entropy.ai">https://cross-entropy.ai</a>
|
| 14 |
</div>
|
|
|
|
| 1 |
+
import { Canvas } from '@/components/canvas';
|
| 2 |
|
| 3 |
export default function Home() {
|
| 4 |
return (
|
|
|
|
| 6 |
<div className="absolute inset-0 z-10 pointer-events-none">
|
| 7 |
<div className="w-full h-full p-6">
|
| 8 |
<div className="text-6xl font-bold">Rubik's Cube Solver</div>
|
| 9 |
+
<div className="text-2xl text-gray-800">with Reinforcement Learning</div>
|
|
|
|
|
|
|
| 10 |
<div className="text-gray-700">
|
| 11 |
<a href="https://cross-entropy.ai">https://cross-entropy.ai</a>
|
| 12 |
</div>
|
src/components/canvas.tsx
CHANGED
|
@@ -1,34 +1,31 @@
|
|
| 1 |
-
|
| 2 |
|
| 3 |
-
import {
|
| 4 |
-
import { Canvas as ThreeCanvas } from
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
import { Env } from
|
| 7 |
-
import {
|
| 8 |
-
import {
|
| 9 |
-
import { PresetsType } from "@react-three/drei/helpers/environment-assets";
|
| 10 |
-
import { Actions } from "./consts";
|
| 11 |
|
| 12 |
export const Canvas = () => {
|
| 13 |
const rubiksCubeRef = useRef<RubiksCubeRef>(null);
|
| 14 |
|
| 15 |
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
| 16 |
const [_, startTransition] = useTransition();
|
| 17 |
-
const [background, setBackground] = useState<PresetsType>(
|
| 18 |
|
| 19 |
const { cubeRoughness, cubeSpeed } = useControls({
|
| 20 |
cubeRoughness: { value: 0.5, min: 0.2, max: 0.8 },
|
| 21 |
cubeSpeed: { value: 2, min: 1, max: 10 },
|
| 22 |
background: {
|
| 23 |
value: background,
|
| 24 |
-
options: [
|
| 25 |
onChange: (value) => startTransition(() => setBackground(value)),
|
| 26 |
},
|
| 27 |
Scramble: button(() => {
|
| 28 |
-
const scrambleSteps = Array.from(
|
| 29 |
-
{ length: 20 },
|
| 30 |
-
() => Actions[Math.floor(Math.random() * Actions.length)],
|
| 31 |
-
);
|
| 32 |
rubiksCubeRef.current?.rotate(scrambleSteps);
|
| 33 |
}),
|
| 34 |
});
|
|
@@ -37,11 +34,7 @@ export const Canvas = () => {
|
|
| 37 |
<>
|
| 38 |
<Leva />
|
| 39 |
<ThreeCanvas shadows camera={{ position: [0, 0, 4.5], fov: 50 }}>
|
| 40 |
-
<RubiksCube
|
| 41 |
-
ref={rubiksCubeRef}
|
| 42 |
-
cubeRoughness={cubeRoughness}
|
| 43 |
-
cubeSpeed={cubeSpeed}
|
| 44 |
-
/>
|
| 45 |
<Env background={background} />
|
| 46 |
</ThreeCanvas>
|
| 47 |
</>
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
|
| 3 |
+
import { PresetsType } from '@react-three/drei/helpers/environment-assets';
|
| 4 |
+
import { Canvas as ThreeCanvas } from '@react-three/fiber';
|
| 5 |
+
import { Leva, button, useControls } from 'leva';
|
| 6 |
+
import { useRef, useState, useTransition } from 'react';
|
| 7 |
|
| 8 |
+
import { Env } from '../components/env';
|
| 9 |
+
import { Actions } from './consts';
|
| 10 |
+
import { RubiksCube, RubiksCubeRef } from './rubiks-cube';
|
|
|
|
|
|
|
| 11 |
|
| 12 |
export const Canvas = () => {
|
| 13 |
const rubiksCubeRef = useRef<RubiksCubeRef>(null);
|
| 14 |
|
| 15 |
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
| 16 |
const [_, startTransition] = useTransition();
|
| 17 |
+
const [background, setBackground] = useState<PresetsType>('sunset');
|
| 18 |
|
| 19 |
const { cubeRoughness, cubeSpeed } = useControls({
|
| 20 |
cubeRoughness: { value: 0.5, min: 0.2, max: 0.8 },
|
| 21 |
cubeSpeed: { value: 2, min: 1, max: 10 },
|
| 22 |
background: {
|
| 23 |
value: background,
|
| 24 |
+
options: ['sunset', 'dawn', 'forest'],
|
| 25 |
onChange: (value) => startTransition(() => setBackground(value)),
|
| 26 |
},
|
| 27 |
Scramble: button(() => {
|
| 28 |
+
const scrambleSteps = Array.from({ length: 20 }, () => Actions[Math.floor(Math.random() * Actions.length)]);
|
|
|
|
|
|
|
|
|
|
| 29 |
rubiksCubeRef.current?.rotate(scrambleSteps);
|
| 30 |
}),
|
| 31 |
});
|
|
|
|
| 34 |
<>
|
| 35 |
<Leva />
|
| 36 |
<ThreeCanvas shadows camera={{ position: [0, 0, 4.5], fov: 50 }}>
|
| 37 |
+
<RubiksCube ref={rubiksCubeRef} cubeRoughness={cubeRoughness} cubeSpeed={cubeSpeed} />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
<Env background={background} />
|
| 39 |
</ThreeCanvas>
|
| 40 |
</>
|
src/components/consts.tsx
CHANGED
|
@@ -1,12 +1,6 @@
|
|
| 1 |
-
export type FacingDirection =
|
| 2 |
-
| "front"
|
| 3 |
-
| "back"
|
| 4 |
-
| "left"
|
| 5 |
-
| "right"
|
| 6 |
-
| "top"
|
| 7 |
-
| "bottom";
|
| 8 |
|
| 9 |
-
export type RotationDirection =
|
| 10 |
|
| 11 |
export const Rotations: Record<FacingDirection, [number, number, number]> = {
|
| 12 |
front: [0, 0, 0],
|
|
@@ -23,16 +17,16 @@ export type RotationStep = {
|
|
| 23 |
};
|
| 24 |
|
| 25 |
export const Actions: Array<RotationStep> = [
|
| 26 |
-
{ faceDirection:
|
| 27 |
-
{ faceDirection:
|
| 28 |
-
{ faceDirection:
|
| 29 |
-
{ faceDirection:
|
| 30 |
-
{ faceDirection:
|
| 31 |
-
{ faceDirection:
|
| 32 |
-
{ faceDirection:
|
| 33 |
-
{ faceDirection:
|
| 34 |
-
{ faceDirection:
|
| 35 |
-
{ faceDirection:
|
| 36 |
-
{ faceDirection:
|
| 37 |
-
{ faceDirection:
|
| 38 |
];
|
|
|
|
| 1 |
+
export type FacingDirection = 'front' | 'back' | 'left' | 'right' | 'top' | 'bottom';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
export type RotationDirection = 'clockwise' | 'counter-clockwise';
|
| 4 |
|
| 5 |
export const Rotations: Record<FacingDirection, [number, number, number]> = {
|
| 6 |
front: [0, 0, 0],
|
|
|
|
| 17 |
};
|
| 18 |
|
| 19 |
export const Actions: Array<RotationStep> = [
|
| 20 |
+
{ faceDirection: 'front', direction: 'clockwise' },
|
| 21 |
+
{ faceDirection: 'front', direction: 'counter-clockwise' },
|
| 22 |
+
{ faceDirection: 'right', direction: 'clockwise' },
|
| 23 |
+
{ faceDirection: 'right', direction: 'counter-clockwise' },
|
| 24 |
+
{ faceDirection: 'left', direction: 'clockwise' },
|
| 25 |
+
{ faceDirection: 'left', direction: 'counter-clockwise' },
|
| 26 |
+
{ faceDirection: 'back', direction: 'clockwise' },
|
| 27 |
+
{ faceDirection: 'back', direction: 'counter-clockwise' },
|
| 28 |
+
{ faceDirection: 'top', direction: 'clockwise' },
|
| 29 |
+
{ faceDirection: 'top', direction: 'counter-clockwise' },
|
| 30 |
+
{ faceDirection: 'bottom', direction: 'clockwise' },
|
| 31 |
+
{ faceDirection: 'bottom', direction: 'counter-clockwise' },
|
| 32 |
];
|
src/components/cube-piece.tsx
CHANGED
|
@@ -1,19 +1,20 @@
|
|
| 1 |
-
|
| 2 |
|
| 3 |
-
import { RoundedBox } from
|
| 4 |
-
import { useEffect, useRef, useState } from
|
| 5 |
-
import {
|
| 6 |
-
|
| 7 |
-
import {
|
|
|
|
| 8 |
|
| 9 |
// Standard Rubik's cube colors
|
| 10 |
const CUBE_COLORS = {
|
| 11 |
-
front:
|
| 12 |
-
back:
|
| 13 |
-
left:
|
| 14 |
-
right:
|
| 15 |
-
top:
|
| 16 |
-
bottom:
|
| 17 |
};
|
| 18 |
|
| 19 |
type CubePieceProps = {
|
|
@@ -52,28 +53,16 @@ export const CubePiece = ({ roughness, initialPosition }: CubePieceProps) => {
|
|
| 52 |
return (
|
| 53 |
<mesh position={position} ref={meshRef}>
|
| 54 |
<RoundedBox args={[0.95, 0.95, 0.95]} radius={0.05} smoothness={4}>
|
| 55 |
-
<meshStandardMaterial
|
| 56 |
-
color="#2a2a2a"
|
| 57 |
-
metalness={1}
|
| 58 |
-
roughness={roughness}
|
| 59 |
-
/>
|
| 60 |
</RoundedBox>
|
| 61 |
|
| 62 |
{Object.entries(visibleFaces).map(([face, isVisible]) => {
|
| 63 |
if (!isVisible) return null;
|
| 64 |
const color = CUBE_COLORS[face as keyof typeof CUBE_COLORS];
|
| 65 |
return (
|
| 66 |
-
<mesh
|
| 67 |
-
key={face}
|
| 68 |
-
position={positions[face as FacingDirection]}
|
| 69 |
-
rotation={Rotations[face as FacingDirection]}
|
| 70 |
-
>
|
| 71 |
<planeGeometry args={[0.8, 0.8]} />
|
| 72 |
-
<meshStandardMaterial
|
| 73 |
-
color={color}
|
| 74 |
-
metalness={1}
|
| 75 |
-
roughness={roughness}
|
| 76 |
-
/>
|
| 77 |
</mesh>
|
| 78 |
);
|
| 79 |
})}
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
|
| 3 |
+
import { RoundedBox } from '@react-three/drei';
|
| 4 |
+
import { useEffect, useRef, useState } from 'react';
|
| 5 |
+
import { Mesh } from 'three';
|
| 6 |
+
|
| 7 |
+
import { FacingDirection, Rotations } from './consts';
|
| 8 |
+
import { rotationController } from './rotation-controller';
|
| 9 |
|
| 10 |
// Standard Rubik's cube colors
|
| 11 |
const CUBE_COLORS = {
|
| 12 |
+
front: '#ff0000', // Red
|
| 13 |
+
back: '#ff00ff', // Purple
|
| 14 |
+
left: '#00ff00', // Green
|
| 15 |
+
right: '#0000ff', // Blue
|
| 16 |
+
top: '#ffff00', // Yellow
|
| 17 |
+
bottom: '#ffffff', // White
|
| 18 |
};
|
| 19 |
|
| 20 |
type CubePieceProps = {
|
|
|
|
| 53 |
return (
|
| 54 |
<mesh position={position} ref={meshRef}>
|
| 55 |
<RoundedBox args={[0.95, 0.95, 0.95]} radius={0.05} smoothness={4}>
|
| 56 |
+
<meshStandardMaterial color="#2a2a2a" metalness={1} roughness={roughness} />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
</RoundedBox>
|
| 58 |
|
| 59 |
{Object.entries(visibleFaces).map(([face, isVisible]) => {
|
| 60 |
if (!isVisible) return null;
|
| 61 |
const color = CUBE_COLORS[face as keyof typeof CUBE_COLORS];
|
| 62 |
return (
|
| 63 |
+
<mesh key={face} position={positions[face as FacingDirection]} rotation={Rotations[face as FacingDirection]}>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
<planeGeometry args={[0.8, 0.8]} />
|
| 65 |
+
<meshStandardMaterial color={color} metalness={1} roughness={roughness} />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
</mesh>
|
| 67 |
);
|
| 68 |
})}
|
src/components/env.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
import { Environment, CameraControls } from "@react-three/drei";
|
| 4 |
-
import { PresetsType } from "@react-three/drei/helpers/environment-assets";
|
| 5 |
-
import { CameraControlsImpl } from "@react-three/drei";
|
| 6 |
const { ACTION } = CameraControlsImpl;
|
| 7 |
|
| 8 |
type EnvProps = {
|
|
|
|
| 1 |
+
'ue client';
|
| 2 |
+
|
| 3 |
+
import { CameraControls, Environment } from '@react-three/drei';
|
| 4 |
+
import { CameraControlsImpl } from '@react-three/drei';
|
| 5 |
+
import { PresetsType } from '@react-three/drei/helpers/environment-assets';
|
| 6 |
|
|
|
|
|
|
|
|
|
|
| 7 |
const { ACTION } = CameraControlsImpl;
|
| 8 |
|
| 9 |
type EnvProps = {
|
src/components/rotation-controller.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
-
import {
|
| 2 |
-
|
|
|
|
| 3 |
|
| 4 |
export class RotationController {
|
| 5 |
private static instance: RotationController;
|
|
@@ -21,31 +22,31 @@ export class RotationController {
|
|
| 21 |
|
| 22 |
private rotate(step: RotationStep, group: Group, delta: number) {
|
| 23 |
let sign = 0;
|
| 24 |
-
let axis:
|
| 25 |
switch (step.faceDirection) {
|
| 26 |
-
case
|
| 27 |
-
sign = step.direction ===
|
| 28 |
-
axis =
|
| 29 |
break;
|
| 30 |
-
case
|
| 31 |
-
sign = step.direction ===
|
| 32 |
-
axis =
|
| 33 |
break;
|
| 34 |
-
case
|
| 35 |
-
sign = step.direction ===
|
| 36 |
-
axis =
|
| 37 |
break;
|
| 38 |
-
case
|
| 39 |
-
sign = step.direction ===
|
| 40 |
-
axis =
|
| 41 |
break;
|
| 42 |
-
case
|
| 43 |
-
sign = step.direction ===
|
| 44 |
-
axis =
|
| 45 |
break;
|
| 46 |
-
case
|
| 47 |
-
sign = step.direction ===
|
| 48 |
-
axis =
|
| 49 |
break;
|
| 50 |
}
|
| 51 |
|
|
@@ -76,17 +77,17 @@ export class RotationController {
|
|
| 76 |
|
| 77 |
getCubes(faceDirection: FacingDirection) {
|
| 78 |
switch (faceDirection) {
|
| 79 |
-
case
|
| 80 |
return this.cubes.filter((m) => m.position.z > 0);
|
| 81 |
-
case
|
| 82 |
return this.cubes.filter((m) => m.position.z < 0);
|
| 83 |
-
case
|
| 84 |
return this.cubes.filter((m) => m.position.x < 0);
|
| 85 |
-
case
|
| 86 |
return this.cubes.filter((m) => m.position.x > 0);
|
| 87 |
-
case
|
| 88 |
return this.cubes.filter((m) => m.position.y > 0);
|
| 89 |
-
case
|
| 90 |
return this.cubes.filter((m) => m.position.y < 0);
|
| 91 |
}
|
| 92 |
}
|
|
@@ -123,3 +124,4 @@ export class RotationController {
|
|
| 123 |
}
|
| 124 |
|
| 125 |
export const rotationController = RotationController.getInstance();
|
|
|
|
|
|
| 1 |
+
import { Group, Mesh } from 'three';
|
| 2 |
+
|
| 3 |
+
import { FacingDirection, RotationStep } from './consts';
|
| 4 |
|
| 5 |
export class RotationController {
|
| 6 |
private static instance: RotationController;
|
|
|
|
| 22 |
|
| 23 |
private rotate(step: RotationStep, group: Group, delta: number) {
|
| 24 |
let sign = 0;
|
| 25 |
+
let axis: 'x' | 'y' | 'z' = 'x';
|
| 26 |
switch (step.faceDirection) {
|
| 27 |
+
case 'front':
|
| 28 |
+
sign = step.direction === 'clockwise' ? -1 : 1;
|
| 29 |
+
axis = 'z';
|
| 30 |
break;
|
| 31 |
+
case 'back':
|
| 32 |
+
sign = step.direction === 'clockwise' ? 1 : -1;
|
| 33 |
+
axis = 'z';
|
| 34 |
break;
|
| 35 |
+
case 'left':
|
| 36 |
+
sign = step.direction === 'clockwise' ? 1 : -1;
|
| 37 |
+
axis = 'x';
|
| 38 |
break;
|
| 39 |
+
case 'right':
|
| 40 |
+
sign = step.direction === 'clockwise' ? -1 : 1;
|
| 41 |
+
axis = 'x';
|
| 42 |
break;
|
| 43 |
+
case 'top':
|
| 44 |
+
sign = step.direction === 'clockwise' ? -1 : 1;
|
| 45 |
+
axis = 'y';
|
| 46 |
break;
|
| 47 |
+
case 'bottom':
|
| 48 |
+
sign = step.direction === 'clockwise' ? 1 : -1;
|
| 49 |
+
axis = 'y';
|
| 50 |
break;
|
| 51 |
}
|
| 52 |
|
|
|
|
| 77 |
|
| 78 |
getCubes(faceDirection: FacingDirection) {
|
| 79 |
switch (faceDirection) {
|
| 80 |
+
case 'front':
|
| 81 |
return this.cubes.filter((m) => m.position.z > 0);
|
| 82 |
+
case 'back':
|
| 83 |
return this.cubes.filter((m) => m.position.z < 0);
|
| 84 |
+
case 'left':
|
| 85 |
return this.cubes.filter((m) => m.position.x < 0);
|
| 86 |
+
case 'right':
|
| 87 |
return this.cubes.filter((m) => m.position.x > 0);
|
| 88 |
+
case 'top':
|
| 89 |
return this.cubes.filter((m) => m.position.y > 0);
|
| 90 |
+
case 'bottom':
|
| 91 |
return this.cubes.filter((m) => m.position.y < 0);
|
| 92 |
}
|
| 93 |
}
|
|
|
|
| 124 |
}
|
| 125 |
|
| 126 |
export const rotationController = RotationController.getInstance();
|
| 127 |
+
export const frameCallback = rotationController.frameCallback.bind(rotationController);
|
src/components/rotation-panel.tsx
CHANGED
|
@@ -1,14 +1,10 @@
|
|
| 1 |
-
|
| 2 |
|
| 3 |
-
import { useLoader } from
|
| 4 |
-
import {
|
| 5 |
-
import {
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
RotationStep,
|
| 9 |
-
RotationDirection,
|
| 10 |
-
} from "./consts";
|
| 11 |
-
import { useState } from "react";
|
| 12 |
|
| 13 |
type RotationPanelProps = {
|
| 14 |
facingDirection: FacingDirection;
|
|
@@ -16,12 +12,8 @@ type RotationPanelProps = {
|
|
| 16 |
onClick?: (step: RotationStep) => void;
|
| 17 |
};
|
| 18 |
|
| 19 |
-
export const RotationPanel = ({
|
| 20 |
-
|
| 21 |
-
direction,
|
| 22 |
-
onClick,
|
| 23 |
-
}: RotationPanelProps) => {
|
| 24 |
-
const clockwise = direction === "clockwise";
|
| 25 |
const texture = useLoader(TextureLoader, `/textures/${direction}.png`);
|
| 26 |
const [opacity, setOpacity] = useState(0);
|
| 27 |
|
|
@@ -34,8 +26,7 @@ export const RotationPanel = ({
|
|
| 34 |
bottom: clockwise ? [0.5, -1.01, 0] : [-0.5, -1.01, 0],
|
| 35 |
};
|
| 36 |
|
| 37 |
-
const handleClick = () =>
|
| 38 |
-
onClick?.({ faceDirection: facingDirection, direction });
|
| 39 |
|
| 40 |
return (
|
| 41 |
<mesh
|
|
@@ -43,22 +34,16 @@ export const RotationPanel = ({
|
|
| 43 |
rotation={Rotations[facingDirection]}
|
| 44 |
onPointerEnter={() => {
|
| 45 |
setOpacity(1);
|
| 46 |
-
document.body.style.cursor =
|
| 47 |
}}
|
| 48 |
onPointerLeave={() => {
|
| 49 |
setOpacity(0);
|
| 50 |
-
document.body.style.cursor =
|
| 51 |
}}
|
| 52 |
onClick={handleClick}
|
| 53 |
>
|
| 54 |
<planeGeometry args={[0.8, 1.6]} />
|
| 55 |
-
<meshStandardMaterial
|
| 56 |
-
color={"#aaaaaa"}
|
| 57 |
-
roughness={0.5}
|
| 58 |
-
opacity={opacity}
|
| 59 |
-
map={texture}
|
| 60 |
-
transparent
|
| 61 |
-
/>
|
| 62 |
</mesh>
|
| 63 |
);
|
| 64 |
};
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
|
| 3 |
+
import { useLoader } from '@react-three/fiber';
|
| 4 |
+
import { useState } from 'react';
|
| 5 |
+
import { TextureLoader } from 'three';
|
| 6 |
+
|
| 7 |
+
import { FacingDirection, RotationDirection, RotationStep, Rotations } from './consts';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
type RotationPanelProps = {
|
| 10 |
facingDirection: FacingDirection;
|
|
|
|
| 12 |
onClick?: (step: RotationStep) => void;
|
| 13 |
};
|
| 14 |
|
| 15 |
+
export const RotationPanel = ({ facingDirection, direction, onClick }: RotationPanelProps) => {
|
| 16 |
+
const clockwise = direction === 'clockwise';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
const texture = useLoader(TextureLoader, `/textures/${direction}.png`);
|
| 18 |
const [opacity, setOpacity] = useState(0);
|
| 19 |
|
|
|
|
| 26 |
bottom: clockwise ? [0.5, -1.01, 0] : [-0.5, -1.01, 0],
|
| 27 |
};
|
| 28 |
|
| 29 |
+
const handleClick = () => onClick?.({ faceDirection: facingDirection, direction });
|
|
|
|
| 30 |
|
| 31 |
return (
|
| 32 |
<mesh
|
|
|
|
| 34 |
rotation={Rotations[facingDirection]}
|
| 35 |
onPointerEnter={() => {
|
| 36 |
setOpacity(1);
|
| 37 |
+
document.body.style.cursor = 'pointer';
|
| 38 |
}}
|
| 39 |
onPointerLeave={() => {
|
| 40 |
setOpacity(0);
|
| 41 |
+
document.body.style.cursor = 'default';
|
| 42 |
}}
|
| 43 |
onClick={handleClick}
|
| 44 |
>
|
| 45 |
<planeGeometry args={[0.8, 1.6]} />
|
| 46 |
+
<meshStandardMaterial color={'#aaaaaa'} roughness={0.5} opacity={opacity} map={texture} transparent />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
</mesh>
|
| 48 |
);
|
| 49 |
};
|
src/components/rotator.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import {
|
| 3 |
-
|
| 4 |
-
import {
|
| 5 |
-
import {
|
|
|
|
| 6 |
|
| 7 |
export type RotatorRef = {
|
| 8 |
rotate: (steps: Array<RotationStep>) => void;
|
|
@@ -12,38 +13,33 @@ type RotatorProps = {
|
|
| 12 |
cubeSpeed: number;
|
| 13 |
};
|
| 14 |
|
| 15 |
-
export const Rotator = forwardRef<RotatorRef, RotatorProps>(
|
| 16 |
-
(
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
);
|
| 46 |
-
},
|
| 47 |
-
);
|
| 48 |
-
|
| 49 |
-
Rotator.displayName = "Rotator";
|
|
|
|
| 1 |
+
import { useFrame } from '@react-three/fiber';
|
| 2 |
+
import { Fragment, forwardRef, useImperativeHandle } from 'react';
|
| 3 |
+
|
| 4 |
+
import { FacingDirection, RotationStep } from './consts';
|
| 5 |
+
import { frameCallback, rotationController } from './rotation-controller';
|
| 6 |
+
import { RotationPanel } from './rotation-panel';
|
| 7 |
|
| 8 |
export type RotatorRef = {
|
| 9 |
rotate: (steps: Array<RotationStep>) => void;
|
|
|
|
| 13 |
cubeSpeed: number;
|
| 14 |
};
|
| 15 |
|
| 16 |
+
export const Rotator = forwardRef<RotatorRef, RotatorProps>(({ cubeSpeed }: RotatorProps, ref) => {
|
| 17 |
+
rotationController.setCubeSpeed(cubeSpeed);
|
| 18 |
+
|
| 19 |
+
useImperativeHandle(ref, () => ({
|
| 20 |
+
rotate: (steps: Array<RotationStep>) => rotationController.addRotationStep(...steps),
|
| 21 |
+
}));
|
| 22 |
+
|
| 23 |
+
useFrame(frameCallback);
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<>
|
| 27 |
+
{['front', 'back', 'left', 'right', 'top', 'bottom'].map((facingDirection) => (
|
| 28 |
+
<Fragment key={facingDirection}>
|
| 29 |
+
<RotationPanel
|
| 30 |
+
direction="clockwise"
|
| 31 |
+
facingDirection={facingDirection as FacingDirection}
|
| 32 |
+
onClick={(s) => rotationController.addRotationStep(s)}
|
| 33 |
+
/>
|
| 34 |
+
<RotationPanel
|
| 35 |
+
direction="counter-clockwise"
|
| 36 |
+
facingDirection={facingDirection as FacingDirection}
|
| 37 |
+
onClick={(s) => rotationController.addRotationStep(s)}
|
| 38 |
+
/>
|
| 39 |
+
</Fragment>
|
| 40 |
+
))}
|
| 41 |
+
</>
|
| 42 |
+
);
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
Rotator.displayName = 'Rotator';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/components/rubiks-cube.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import {
|
| 3 |
-
|
| 4 |
-
import {
|
| 5 |
-
import {
|
| 6 |
-
import { rotationController } from
|
|
|
|
| 7 |
|
| 8 |
const CUBE_POSITIONS: Array<[number, number, number]> = [];
|
| 9 |
for (let x = -0.5; x <= 0.5; x += 1) {
|
|
@@ -23,35 +24,28 @@ type RubiksCubeProps = {
|
|
| 23 |
cubeSpeed: number;
|
| 24 |
};
|
| 25 |
|
| 26 |
-
export const RubiksCube = forwardRef<RubiksCubeRef, RubiksCubeProps>(
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
<Rotator ref={rotatorRef} cubeSpeed={cubeSpeed} />
|
| 52 |
-
</>
|
| 53 |
-
);
|
| 54 |
-
},
|
| 55 |
-
);
|
| 56 |
-
|
| 57 |
-
RubiksCube.displayName = "RubiksCube";
|
|
|
|
| 1 |
+
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
| 2 |
+
import { Group } from 'three';
|
| 3 |
+
|
| 4 |
+
import { RotationStep } from './consts';
|
| 5 |
+
import { CubePiece } from './cube-piece';
|
| 6 |
+
import { rotationController } from './rotation-controller';
|
| 7 |
+
import { Rotator, RotatorRef } from './rotator';
|
| 8 |
|
| 9 |
const CUBE_POSITIONS: Array<[number, number, number]> = [];
|
| 10 |
for (let x = -0.5; x <= 0.5; x += 1) {
|
|
|
|
| 24 |
cubeSpeed: number;
|
| 25 |
};
|
| 26 |
|
| 27 |
+
export const RubiksCube = forwardRef<RubiksCubeRef, RubiksCubeProps>(({ cubeRoughness, cubeSpeed }, ref) => {
|
| 28 |
+
const cubeGroupRef = useRef<Group | null>(null);
|
| 29 |
+
const rotatorRef = useRef<RotatorRef | null>(null);
|
| 30 |
+
|
| 31 |
+
useImperativeHandle(ref, () => ({
|
| 32 |
+
rotate: (steps: Array<RotationStep>) => rotatorRef.current?.rotate(steps),
|
| 33 |
+
}));
|
| 34 |
+
|
| 35 |
+
useEffect(() => {
|
| 36 |
+
if (cubeGroupRef.current) rotationController.setCubeGroup(cubeGroupRef.current);
|
| 37 |
+
}, [cubeGroupRef]);
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<>
|
| 41 |
+
<group ref={cubeGroupRef}>
|
| 42 |
+
{CUBE_POSITIONS.map((position) => (
|
| 43 |
+
<CubePiece key={position.join(',')} initialPosition={position} roughness={cubeRoughness} />
|
| 44 |
+
))}
|
| 45 |
+
</group>
|
| 46 |
+
<Rotator ref={rotatorRef} cubeSpeed={cubeSpeed} />
|
| 47 |
+
</>
|
| 48 |
+
);
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
RubiksCube.displayName = 'RubiksCube';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|