Spaces:
Sleeping
Sleeping
Placeholder Rubics Cube on Gradio
Browse files- .gitignore +7 -0
- app.py +12 -0
- frontend/dist/assets/index-8JIoHBJc.js +0 -0
- frontend/dist/assets/index-Dku5yfD6.css +1 -0
- frontend/dist/index.html +13 -0
- frontend/index.html +13 -0
- frontend/main.py +44 -0
- frontend/package-lock.json +1068 -0
- frontend/package.json +20 -0
- frontend/src/README.md +0 -0
- frontend/src/configs/debug-config.ts +21 -0
- frontend/src/configs/global-light-config.ts +9 -0
- frontend/src/configs/scene-config.ts +17 -0
- frontend/src/configs/sounds-config.ts +7 -0
- frontend/src/core/README.md +0 -0
- frontend/src/core/events.ts +12 -0
- frontend/src/core/state.ts +181 -0
- frontend/src/main.ts +70 -0
- frontend/src/scene/README.md +0 -0
- frontend/src/scene/cube/animator.ts +172 -0
- frontend/src/scene/cube/cube.ts +84 -0
- frontend/src/scene/cube/scramble.ts +32 -0
- frontend/src/scene/scene.ts +41 -0
- frontend/src/style.css +105 -0
- frontend/src/ui/README.md +0 -0
- frontend/src/ui/controls/drag-controls.ts +195 -0
- frontend/src/ui/controls/keyboard.ts +40 -0
- frontend/src/ui/debugger.ts +84 -0
- frontend/tsconfig.json +18 -0
- frontend/vite.config.ts +10 -0
- requirements.txt +3 -1
.gitignore
CHANGED
|
@@ -216,3 +216,10 @@ __marimo__/
|
|
| 216 |
|
| 217 |
# Streamlit
|
| 218 |
.streamlit/secrets.toml
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
# Streamlit
|
| 218 |
.streamlit/secrets.toml
|
| 219 |
+
|
| 220 |
+
# Node / Vite (frontend)
|
| 221 |
+
node_modules/
|
| 222 |
+
frontend/node_modules/
|
| 223 |
+
# We commit the built bundle so HF Spaces doesn't need Node:
|
| 224 |
+
!frontend/dist/
|
| 225 |
+
!frontend/dist/**
|
app.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hugging Face Space entrypoint.
|
| 2 |
+
|
| 3 |
+
Re-exports the FastAPI app (which has Gradio mounted at ``/``) from
|
| 4 |
+
``frontend.main`` so HF Spaces can launch it directly.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from frontend.main import app
|
| 8 |
+
|
| 9 |
+
if __name__ == "__main__":
|
| 10 |
+
import uvicorn
|
| 11 |
+
|
| 12 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
frontend/dist/assets/index-8JIoHBJc.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/dist/assets/index-Dku5yfD6.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.threejs-canvas,.pixi-canvas{position:fixed;top:0;left:0;outline:none}html,body{margin:0;padding:0;height:100%;background:#999;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:#111;overflow:hidden}#app{width:100vw;height:100vh}#debugger{position:fixed;top:12px;right:12px;background:#141414c7;color:#eee;padding:12px 14px;border-radius:8px;font-size:12px;line-height:1.4;min-width:220px;max-width:280px;box-shadow:0 6px 20px #00000059;user-select:none;z-index:10}#debugger h3{margin:0 0 6px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#9cf}#debugger .row{display:flex;justify-content:space-between;gap:8px}#debugger .solved-yes{color:#6f6}#debugger .solved-no{color:#f88}#debugger .faces{display:grid;grid-template-columns:repeat(4,auto);gap:6px;margin:8px 0}#debugger .face{display:grid;grid-template-columns:repeat(3,10px);grid-template-rows:repeat(3,10px);gap:1px;padding:2px;background:#000;border-radius:2px}#debugger .face .label{grid-column:1 / span 3;text-align:center;font-size:9px;color:#aaa;margin-bottom:1px}#debugger .sticker{width:10px;height:10px;border-radius:1px}#debugger .history{max-height:80px;overflow-y:auto;font-size:11px;background:#00000059;padding:4px 6px;border-radius:4px;white-space:normal;word-break:break-word}#help{position:fixed;bottom:12px;left:12px;background:#141414c7;color:#ddd;padding:8px 12px;border-radius:6px;font-size:11px;line-height:1.5;z-index:10}#help b{color:#9cf}
|
frontend/dist/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
| 6 |
+
<title>Rubik's Cube Instructor</title>
|
| 7 |
+
<script type="module" crossorigin src="./assets/index-8JIoHBJc.js"></script>
|
| 8 |
+
<link rel="stylesheet" crossorigin href="./assets/index-Dku5yfD6.css">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div id="app"></div>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
| 6 |
+
<title>Rubik's Cube Instructor</title>
|
| 7 |
+
<link rel="stylesheet" href="./src/style.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="app"></div>
|
| 11 |
+
<script type="module" src="/src/main.ts"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/main.py
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Gradio entry that serves the built Three.js Rubik's Cube app.
|
| 2 |
+
|
| 3 |
+
The Vite-built bundle in ``frontend/dist/`` is mounted as a FastAPI
|
| 4 |
+
``StaticFiles`` route, then embedded into the Gradio UI via an iframe.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import gradio as gr
|
| 12 |
+
from fastapi import FastAPI
|
| 13 |
+
from fastapi.staticfiles import StaticFiles
|
| 14 |
+
|
| 15 |
+
FRONTEND_DIR = Path(__file__).resolve().parent
|
| 16 |
+
DIST_DIR = FRONTEND_DIR / "dist"
|
| 17 |
+
|
| 18 |
+
app = FastAPI()
|
| 19 |
+
|
| 20 |
+
if DIST_DIR.is_dir():
|
| 21 |
+
app.mount("/cube", StaticFiles(directory=str(DIST_DIR), html=True), name="cube")
|
| 22 |
+
IFRAME_HTML = (
|
| 23 |
+
'<iframe src="/cube/index.html" '
|
| 24 |
+
'style="width:100%;height:88vh;border:0;border-radius:8px;background:#999" '
|
| 25 |
+
'allow="fullscreen"></iframe>'
|
| 26 |
+
)
|
| 27 |
+
else:
|
| 28 |
+
IFRAME_HTML = (
|
| 29 |
+
'<div style="padding:24px;font-family:monospace;background:#fee;border:1px solid #c66;'
|
| 30 |
+
'border-radius:6px">Frontend bundle not found at <code>frontend/dist/</code>. '
|
| 31 |
+
'Run <code>npm install && npm run build</code> in <code>frontend/</code>.</div>'
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
with gr.Blocks(title="Rubik's Cube Instructor", fill_height=True) as demo:
|
| 35 |
+
gr.Markdown("# Rubik's Cube Instructor")
|
| 36 |
+
gr.HTML(IFRAME_HTML)
|
| 37 |
+
|
| 38 |
+
app = gr.mount_gradio_app(app, demo, path="/")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
if __name__ == "__main__":
|
| 42 |
+
import uvicorn
|
| 43 |
+
|
| 44 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
frontend/package-lock.json
ADDED
|
@@ -0,0 +1,1068 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "hf-rubic-instructor-frontend",
|
| 3 |
+
"version": "0.0.1",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "hf-rubic-instructor-frontend",
|
| 9 |
+
"version": "0.0.1",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"@tweenjs/tween.js": "^25.0.0",
|
| 12 |
+
"three": "^0.166.0"
|
| 13 |
+
},
|
| 14 |
+
"devDependencies": {
|
| 15 |
+
"@types/three": "^0.166.0",
|
| 16 |
+
"typescript": "^5.4.5",
|
| 17 |
+
"vite": "^5.3.1"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 21 |
+
"version": "0.21.5",
|
| 22 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
| 23 |
+
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
| 24 |
+
"cpu": [
|
| 25 |
+
"ppc64"
|
| 26 |
+
],
|
| 27 |
+
"dev": true,
|
| 28 |
+
"license": "MIT",
|
| 29 |
+
"optional": true,
|
| 30 |
+
"os": [
|
| 31 |
+
"aix"
|
| 32 |
+
],
|
| 33 |
+
"engines": {
|
| 34 |
+
"node": ">=12"
|
| 35 |
+
}
|
| 36 |
+
},
|
| 37 |
+
"node_modules/@esbuild/android-arm": {
|
| 38 |
+
"version": "0.21.5",
|
| 39 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
| 40 |
+
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
| 41 |
+
"cpu": [
|
| 42 |
+
"arm"
|
| 43 |
+
],
|
| 44 |
+
"dev": true,
|
| 45 |
+
"license": "MIT",
|
| 46 |
+
"optional": true,
|
| 47 |
+
"os": [
|
| 48 |
+
"android"
|
| 49 |
+
],
|
| 50 |
+
"engines": {
|
| 51 |
+
"node": ">=12"
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"node_modules/@esbuild/android-arm64": {
|
| 55 |
+
"version": "0.21.5",
|
| 56 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
| 57 |
+
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
| 58 |
+
"cpu": [
|
| 59 |
+
"arm64"
|
| 60 |
+
],
|
| 61 |
+
"dev": true,
|
| 62 |
+
"license": "MIT",
|
| 63 |
+
"optional": true,
|
| 64 |
+
"os": [
|
| 65 |
+
"android"
|
| 66 |
+
],
|
| 67 |
+
"engines": {
|
| 68 |
+
"node": ">=12"
|
| 69 |
+
}
|
| 70 |
+
},
|
| 71 |
+
"node_modules/@esbuild/android-x64": {
|
| 72 |
+
"version": "0.21.5",
|
| 73 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
| 74 |
+
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
| 75 |
+
"cpu": [
|
| 76 |
+
"x64"
|
| 77 |
+
],
|
| 78 |
+
"dev": true,
|
| 79 |
+
"license": "MIT",
|
| 80 |
+
"optional": true,
|
| 81 |
+
"os": [
|
| 82 |
+
"android"
|
| 83 |
+
],
|
| 84 |
+
"engines": {
|
| 85 |
+
"node": ">=12"
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 89 |
+
"version": "0.21.5",
|
| 90 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
| 91 |
+
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
| 92 |
+
"cpu": [
|
| 93 |
+
"arm64"
|
| 94 |
+
],
|
| 95 |
+
"dev": true,
|
| 96 |
+
"license": "MIT",
|
| 97 |
+
"optional": true,
|
| 98 |
+
"os": [
|
| 99 |
+
"darwin"
|
| 100 |
+
],
|
| 101 |
+
"engines": {
|
| 102 |
+
"node": ">=12"
|
| 103 |
+
}
|
| 104 |
+
},
|
| 105 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 106 |
+
"version": "0.21.5",
|
| 107 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
| 108 |
+
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
| 109 |
+
"cpu": [
|
| 110 |
+
"x64"
|
| 111 |
+
],
|
| 112 |
+
"dev": true,
|
| 113 |
+
"license": "MIT",
|
| 114 |
+
"optional": true,
|
| 115 |
+
"os": [
|
| 116 |
+
"darwin"
|
| 117 |
+
],
|
| 118 |
+
"engines": {
|
| 119 |
+
"node": ">=12"
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 123 |
+
"version": "0.21.5",
|
| 124 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
| 125 |
+
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
| 126 |
+
"cpu": [
|
| 127 |
+
"arm64"
|
| 128 |
+
],
|
| 129 |
+
"dev": true,
|
| 130 |
+
"license": "MIT",
|
| 131 |
+
"optional": true,
|
| 132 |
+
"os": [
|
| 133 |
+
"freebsd"
|
| 134 |
+
],
|
| 135 |
+
"engines": {
|
| 136 |
+
"node": ">=12"
|
| 137 |
+
}
|
| 138 |
+
},
|
| 139 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 140 |
+
"version": "0.21.5",
|
| 141 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
| 142 |
+
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
| 143 |
+
"cpu": [
|
| 144 |
+
"x64"
|
| 145 |
+
],
|
| 146 |
+
"dev": true,
|
| 147 |
+
"license": "MIT",
|
| 148 |
+
"optional": true,
|
| 149 |
+
"os": [
|
| 150 |
+
"freebsd"
|
| 151 |
+
],
|
| 152 |
+
"engines": {
|
| 153 |
+
"node": ">=12"
|
| 154 |
+
}
|
| 155 |
+
},
|
| 156 |
+
"node_modules/@esbuild/linux-arm": {
|
| 157 |
+
"version": "0.21.5",
|
| 158 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
| 159 |
+
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
| 160 |
+
"cpu": [
|
| 161 |
+
"arm"
|
| 162 |
+
],
|
| 163 |
+
"dev": true,
|
| 164 |
+
"license": "MIT",
|
| 165 |
+
"optional": true,
|
| 166 |
+
"os": [
|
| 167 |
+
"linux"
|
| 168 |
+
],
|
| 169 |
+
"engines": {
|
| 170 |
+
"node": ">=12"
|
| 171 |
+
}
|
| 172 |
+
},
|
| 173 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 174 |
+
"version": "0.21.5",
|
| 175 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
| 176 |
+
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
| 177 |
+
"cpu": [
|
| 178 |
+
"arm64"
|
| 179 |
+
],
|
| 180 |
+
"dev": true,
|
| 181 |
+
"license": "MIT",
|
| 182 |
+
"optional": true,
|
| 183 |
+
"os": [
|
| 184 |
+
"linux"
|
| 185 |
+
],
|
| 186 |
+
"engines": {
|
| 187 |
+
"node": ">=12"
|
| 188 |
+
}
|
| 189 |
+
},
|
| 190 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 191 |
+
"version": "0.21.5",
|
| 192 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
| 193 |
+
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
| 194 |
+
"cpu": [
|
| 195 |
+
"ia32"
|
| 196 |
+
],
|
| 197 |
+
"dev": true,
|
| 198 |
+
"license": "MIT",
|
| 199 |
+
"optional": true,
|
| 200 |
+
"os": [
|
| 201 |
+
"linux"
|
| 202 |
+
],
|
| 203 |
+
"engines": {
|
| 204 |
+
"node": ">=12"
|
| 205 |
+
}
|
| 206 |
+
},
|
| 207 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 208 |
+
"version": "0.21.5",
|
| 209 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
| 210 |
+
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
| 211 |
+
"cpu": [
|
| 212 |
+
"loong64"
|
| 213 |
+
],
|
| 214 |
+
"dev": true,
|
| 215 |
+
"license": "MIT",
|
| 216 |
+
"optional": true,
|
| 217 |
+
"os": [
|
| 218 |
+
"linux"
|
| 219 |
+
],
|
| 220 |
+
"engines": {
|
| 221 |
+
"node": ">=12"
|
| 222 |
+
}
|
| 223 |
+
},
|
| 224 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 225 |
+
"version": "0.21.5",
|
| 226 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
| 227 |
+
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
| 228 |
+
"cpu": [
|
| 229 |
+
"mips64el"
|
| 230 |
+
],
|
| 231 |
+
"dev": true,
|
| 232 |
+
"license": "MIT",
|
| 233 |
+
"optional": true,
|
| 234 |
+
"os": [
|
| 235 |
+
"linux"
|
| 236 |
+
],
|
| 237 |
+
"engines": {
|
| 238 |
+
"node": ">=12"
|
| 239 |
+
}
|
| 240 |
+
},
|
| 241 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 242 |
+
"version": "0.21.5",
|
| 243 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
| 244 |
+
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
| 245 |
+
"cpu": [
|
| 246 |
+
"ppc64"
|
| 247 |
+
],
|
| 248 |
+
"dev": true,
|
| 249 |
+
"license": "MIT",
|
| 250 |
+
"optional": true,
|
| 251 |
+
"os": [
|
| 252 |
+
"linux"
|
| 253 |
+
],
|
| 254 |
+
"engines": {
|
| 255 |
+
"node": ">=12"
|
| 256 |
+
}
|
| 257 |
+
},
|
| 258 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 259 |
+
"version": "0.21.5",
|
| 260 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
| 261 |
+
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
| 262 |
+
"cpu": [
|
| 263 |
+
"riscv64"
|
| 264 |
+
],
|
| 265 |
+
"dev": true,
|
| 266 |
+
"license": "MIT",
|
| 267 |
+
"optional": true,
|
| 268 |
+
"os": [
|
| 269 |
+
"linux"
|
| 270 |
+
],
|
| 271 |
+
"engines": {
|
| 272 |
+
"node": ">=12"
|
| 273 |
+
}
|
| 274 |
+
},
|
| 275 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 276 |
+
"version": "0.21.5",
|
| 277 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
| 278 |
+
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
| 279 |
+
"cpu": [
|
| 280 |
+
"s390x"
|
| 281 |
+
],
|
| 282 |
+
"dev": true,
|
| 283 |
+
"license": "MIT",
|
| 284 |
+
"optional": true,
|
| 285 |
+
"os": [
|
| 286 |
+
"linux"
|
| 287 |
+
],
|
| 288 |
+
"engines": {
|
| 289 |
+
"node": ">=12"
|
| 290 |
+
}
|
| 291 |
+
},
|
| 292 |
+
"node_modules/@esbuild/linux-x64": {
|
| 293 |
+
"version": "0.21.5",
|
| 294 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
| 295 |
+
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
| 296 |
+
"cpu": [
|
| 297 |
+
"x64"
|
| 298 |
+
],
|
| 299 |
+
"dev": true,
|
| 300 |
+
"license": "MIT",
|
| 301 |
+
"optional": true,
|
| 302 |
+
"os": [
|
| 303 |
+
"linux"
|
| 304 |
+
],
|
| 305 |
+
"engines": {
|
| 306 |
+
"node": ">=12"
|
| 307 |
+
}
|
| 308 |
+
},
|
| 309 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 310 |
+
"version": "0.21.5",
|
| 311 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
| 312 |
+
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
| 313 |
+
"cpu": [
|
| 314 |
+
"x64"
|
| 315 |
+
],
|
| 316 |
+
"dev": true,
|
| 317 |
+
"license": "MIT",
|
| 318 |
+
"optional": true,
|
| 319 |
+
"os": [
|
| 320 |
+
"netbsd"
|
| 321 |
+
],
|
| 322 |
+
"engines": {
|
| 323 |
+
"node": ">=12"
|
| 324 |
+
}
|
| 325 |
+
},
|
| 326 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 327 |
+
"version": "0.21.5",
|
| 328 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
| 329 |
+
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
| 330 |
+
"cpu": [
|
| 331 |
+
"x64"
|
| 332 |
+
],
|
| 333 |
+
"dev": true,
|
| 334 |
+
"license": "MIT",
|
| 335 |
+
"optional": true,
|
| 336 |
+
"os": [
|
| 337 |
+
"openbsd"
|
| 338 |
+
],
|
| 339 |
+
"engines": {
|
| 340 |
+
"node": ">=12"
|
| 341 |
+
}
|
| 342 |
+
},
|
| 343 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 344 |
+
"version": "0.21.5",
|
| 345 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
| 346 |
+
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
| 347 |
+
"cpu": [
|
| 348 |
+
"x64"
|
| 349 |
+
],
|
| 350 |
+
"dev": true,
|
| 351 |
+
"license": "MIT",
|
| 352 |
+
"optional": true,
|
| 353 |
+
"os": [
|
| 354 |
+
"sunos"
|
| 355 |
+
],
|
| 356 |
+
"engines": {
|
| 357 |
+
"node": ">=12"
|
| 358 |
+
}
|
| 359 |
+
},
|
| 360 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 361 |
+
"version": "0.21.5",
|
| 362 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
| 363 |
+
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
| 364 |
+
"cpu": [
|
| 365 |
+
"arm64"
|
| 366 |
+
],
|
| 367 |
+
"dev": true,
|
| 368 |
+
"license": "MIT",
|
| 369 |
+
"optional": true,
|
| 370 |
+
"os": [
|
| 371 |
+
"win32"
|
| 372 |
+
],
|
| 373 |
+
"engines": {
|
| 374 |
+
"node": ">=12"
|
| 375 |
+
}
|
| 376 |
+
},
|
| 377 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 378 |
+
"version": "0.21.5",
|
| 379 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
| 380 |
+
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
| 381 |
+
"cpu": [
|
| 382 |
+
"ia32"
|
| 383 |
+
],
|
| 384 |
+
"dev": true,
|
| 385 |
+
"license": "MIT",
|
| 386 |
+
"optional": true,
|
| 387 |
+
"os": [
|
| 388 |
+
"win32"
|
| 389 |
+
],
|
| 390 |
+
"engines": {
|
| 391 |
+
"node": ">=12"
|
| 392 |
+
}
|
| 393 |
+
},
|
| 394 |
+
"node_modules/@esbuild/win32-x64": {
|
| 395 |
+
"version": "0.21.5",
|
| 396 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
| 397 |
+
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
| 398 |
+
"cpu": [
|
| 399 |
+
"x64"
|
| 400 |
+
],
|
| 401 |
+
"dev": true,
|
| 402 |
+
"license": "MIT",
|
| 403 |
+
"optional": true,
|
| 404 |
+
"os": [
|
| 405 |
+
"win32"
|
| 406 |
+
],
|
| 407 |
+
"engines": {
|
| 408 |
+
"node": ">=12"
|
| 409 |
+
}
|
| 410 |
+
},
|
| 411 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 412 |
+
"version": "4.61.1",
|
| 413 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz",
|
| 414 |
+
"integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==",
|
| 415 |
+
"cpu": [
|
| 416 |
+
"arm"
|
| 417 |
+
],
|
| 418 |
+
"dev": true,
|
| 419 |
+
"license": "MIT",
|
| 420 |
+
"optional": true,
|
| 421 |
+
"os": [
|
| 422 |
+
"android"
|
| 423 |
+
]
|
| 424 |
+
},
|
| 425 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 426 |
+
"version": "4.61.1",
|
| 427 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz",
|
| 428 |
+
"integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==",
|
| 429 |
+
"cpu": [
|
| 430 |
+
"arm64"
|
| 431 |
+
],
|
| 432 |
+
"dev": true,
|
| 433 |
+
"license": "MIT",
|
| 434 |
+
"optional": true,
|
| 435 |
+
"os": [
|
| 436 |
+
"android"
|
| 437 |
+
]
|
| 438 |
+
},
|
| 439 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 440 |
+
"version": "4.61.1",
|
| 441 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz",
|
| 442 |
+
"integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==",
|
| 443 |
+
"cpu": [
|
| 444 |
+
"arm64"
|
| 445 |
+
],
|
| 446 |
+
"dev": true,
|
| 447 |
+
"license": "MIT",
|
| 448 |
+
"optional": true,
|
| 449 |
+
"os": [
|
| 450 |
+
"darwin"
|
| 451 |
+
]
|
| 452 |
+
},
|
| 453 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 454 |
+
"version": "4.61.1",
|
| 455 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz",
|
| 456 |
+
"integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==",
|
| 457 |
+
"cpu": [
|
| 458 |
+
"x64"
|
| 459 |
+
],
|
| 460 |
+
"dev": true,
|
| 461 |
+
"license": "MIT",
|
| 462 |
+
"optional": true,
|
| 463 |
+
"os": [
|
| 464 |
+
"darwin"
|
| 465 |
+
]
|
| 466 |
+
},
|
| 467 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 468 |
+
"version": "4.61.1",
|
| 469 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz",
|
| 470 |
+
"integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==",
|
| 471 |
+
"cpu": [
|
| 472 |
+
"arm64"
|
| 473 |
+
],
|
| 474 |
+
"dev": true,
|
| 475 |
+
"license": "MIT",
|
| 476 |
+
"optional": true,
|
| 477 |
+
"os": [
|
| 478 |
+
"freebsd"
|
| 479 |
+
]
|
| 480 |
+
},
|
| 481 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 482 |
+
"version": "4.61.1",
|
| 483 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz",
|
| 484 |
+
"integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==",
|
| 485 |
+
"cpu": [
|
| 486 |
+
"x64"
|
| 487 |
+
],
|
| 488 |
+
"dev": true,
|
| 489 |
+
"license": "MIT",
|
| 490 |
+
"optional": true,
|
| 491 |
+
"os": [
|
| 492 |
+
"freebsd"
|
| 493 |
+
]
|
| 494 |
+
},
|
| 495 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 496 |
+
"version": "4.61.1",
|
| 497 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz",
|
| 498 |
+
"integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==",
|
| 499 |
+
"cpu": [
|
| 500 |
+
"arm"
|
| 501 |
+
],
|
| 502 |
+
"dev": true,
|
| 503 |
+
"license": "MIT",
|
| 504 |
+
"optional": true,
|
| 505 |
+
"os": [
|
| 506 |
+
"linux"
|
| 507 |
+
]
|
| 508 |
+
},
|
| 509 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 510 |
+
"version": "4.61.1",
|
| 511 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz",
|
| 512 |
+
"integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==",
|
| 513 |
+
"cpu": [
|
| 514 |
+
"arm"
|
| 515 |
+
],
|
| 516 |
+
"dev": true,
|
| 517 |
+
"license": "MIT",
|
| 518 |
+
"optional": true,
|
| 519 |
+
"os": [
|
| 520 |
+
"linux"
|
| 521 |
+
]
|
| 522 |
+
},
|
| 523 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 524 |
+
"version": "4.61.1",
|
| 525 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz",
|
| 526 |
+
"integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==",
|
| 527 |
+
"cpu": [
|
| 528 |
+
"arm64"
|
| 529 |
+
],
|
| 530 |
+
"dev": true,
|
| 531 |
+
"license": "MIT",
|
| 532 |
+
"optional": true,
|
| 533 |
+
"os": [
|
| 534 |
+
"linux"
|
| 535 |
+
]
|
| 536 |
+
},
|
| 537 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 538 |
+
"version": "4.61.1",
|
| 539 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz",
|
| 540 |
+
"integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==",
|
| 541 |
+
"cpu": [
|
| 542 |
+
"arm64"
|
| 543 |
+
],
|
| 544 |
+
"dev": true,
|
| 545 |
+
"license": "MIT",
|
| 546 |
+
"optional": true,
|
| 547 |
+
"os": [
|
| 548 |
+
"linux"
|
| 549 |
+
]
|
| 550 |
+
},
|
| 551 |
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 552 |
+
"version": "4.61.1",
|
| 553 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz",
|
| 554 |
+
"integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==",
|
| 555 |
+
"cpu": [
|
| 556 |
+
"loong64"
|
| 557 |
+
],
|
| 558 |
+
"dev": true,
|
| 559 |
+
"license": "MIT",
|
| 560 |
+
"optional": true,
|
| 561 |
+
"os": [
|
| 562 |
+
"linux"
|
| 563 |
+
]
|
| 564 |
+
},
|
| 565 |
+
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
| 566 |
+
"version": "4.61.1",
|
| 567 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz",
|
| 568 |
+
"integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==",
|
| 569 |
+
"cpu": [
|
| 570 |
+
"loong64"
|
| 571 |
+
],
|
| 572 |
+
"dev": true,
|
| 573 |
+
"license": "MIT",
|
| 574 |
+
"optional": true,
|
| 575 |
+
"os": [
|
| 576 |
+
"linux"
|
| 577 |
+
]
|
| 578 |
+
},
|
| 579 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 580 |
+
"version": "4.61.1",
|
| 581 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz",
|
| 582 |
+
"integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==",
|
| 583 |
+
"cpu": [
|
| 584 |
+
"ppc64"
|
| 585 |
+
],
|
| 586 |
+
"dev": true,
|
| 587 |
+
"license": "MIT",
|
| 588 |
+
"optional": true,
|
| 589 |
+
"os": [
|
| 590 |
+
"linux"
|
| 591 |
+
]
|
| 592 |
+
},
|
| 593 |
+
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
| 594 |
+
"version": "4.61.1",
|
| 595 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz",
|
| 596 |
+
"integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==",
|
| 597 |
+
"cpu": [
|
| 598 |
+
"ppc64"
|
| 599 |
+
],
|
| 600 |
+
"dev": true,
|
| 601 |
+
"license": "MIT",
|
| 602 |
+
"optional": true,
|
| 603 |
+
"os": [
|
| 604 |
+
"linux"
|
| 605 |
+
]
|
| 606 |
+
},
|
| 607 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 608 |
+
"version": "4.61.1",
|
| 609 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz",
|
| 610 |
+
"integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==",
|
| 611 |
+
"cpu": [
|
| 612 |
+
"riscv64"
|
| 613 |
+
],
|
| 614 |
+
"dev": true,
|
| 615 |
+
"license": "MIT",
|
| 616 |
+
"optional": true,
|
| 617 |
+
"os": [
|
| 618 |
+
"linux"
|
| 619 |
+
]
|
| 620 |
+
},
|
| 621 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 622 |
+
"version": "4.61.1",
|
| 623 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz",
|
| 624 |
+
"integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==",
|
| 625 |
+
"cpu": [
|
| 626 |
+
"riscv64"
|
| 627 |
+
],
|
| 628 |
+
"dev": true,
|
| 629 |
+
"license": "MIT",
|
| 630 |
+
"optional": true,
|
| 631 |
+
"os": [
|
| 632 |
+
"linux"
|
| 633 |
+
]
|
| 634 |
+
},
|
| 635 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 636 |
+
"version": "4.61.1",
|
| 637 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz",
|
| 638 |
+
"integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==",
|
| 639 |
+
"cpu": [
|
| 640 |
+
"s390x"
|
| 641 |
+
],
|
| 642 |
+
"dev": true,
|
| 643 |
+
"license": "MIT",
|
| 644 |
+
"optional": true,
|
| 645 |
+
"os": [
|
| 646 |
+
"linux"
|
| 647 |
+
]
|
| 648 |
+
},
|
| 649 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 650 |
+
"version": "4.61.1",
|
| 651 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz",
|
| 652 |
+
"integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==",
|
| 653 |
+
"cpu": [
|
| 654 |
+
"x64"
|
| 655 |
+
],
|
| 656 |
+
"dev": true,
|
| 657 |
+
"license": "MIT",
|
| 658 |
+
"optional": true,
|
| 659 |
+
"os": [
|
| 660 |
+
"linux"
|
| 661 |
+
]
|
| 662 |
+
},
|
| 663 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 664 |
+
"version": "4.61.1",
|
| 665 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz",
|
| 666 |
+
"integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==",
|
| 667 |
+
"cpu": [
|
| 668 |
+
"x64"
|
| 669 |
+
],
|
| 670 |
+
"dev": true,
|
| 671 |
+
"license": "MIT",
|
| 672 |
+
"optional": true,
|
| 673 |
+
"os": [
|
| 674 |
+
"linux"
|
| 675 |
+
]
|
| 676 |
+
},
|
| 677 |
+
"node_modules/@rollup/rollup-openbsd-x64": {
|
| 678 |
+
"version": "4.61.1",
|
| 679 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz",
|
| 680 |
+
"integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==",
|
| 681 |
+
"cpu": [
|
| 682 |
+
"x64"
|
| 683 |
+
],
|
| 684 |
+
"dev": true,
|
| 685 |
+
"license": "MIT",
|
| 686 |
+
"optional": true,
|
| 687 |
+
"os": [
|
| 688 |
+
"openbsd"
|
| 689 |
+
]
|
| 690 |
+
},
|
| 691 |
+
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 692 |
+
"version": "4.61.1",
|
| 693 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz",
|
| 694 |
+
"integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==",
|
| 695 |
+
"cpu": [
|
| 696 |
+
"arm64"
|
| 697 |
+
],
|
| 698 |
+
"dev": true,
|
| 699 |
+
"license": "MIT",
|
| 700 |
+
"optional": true,
|
| 701 |
+
"os": [
|
| 702 |
+
"openharmony"
|
| 703 |
+
]
|
| 704 |
+
},
|
| 705 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 706 |
+
"version": "4.61.1",
|
| 707 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz",
|
| 708 |
+
"integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==",
|
| 709 |
+
"cpu": [
|
| 710 |
+
"arm64"
|
| 711 |
+
],
|
| 712 |
+
"dev": true,
|
| 713 |
+
"license": "MIT",
|
| 714 |
+
"optional": true,
|
| 715 |
+
"os": [
|
| 716 |
+
"win32"
|
| 717 |
+
]
|
| 718 |
+
},
|
| 719 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 720 |
+
"version": "4.61.1",
|
| 721 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz",
|
| 722 |
+
"integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==",
|
| 723 |
+
"cpu": [
|
| 724 |
+
"ia32"
|
| 725 |
+
],
|
| 726 |
+
"dev": true,
|
| 727 |
+
"license": "MIT",
|
| 728 |
+
"optional": true,
|
| 729 |
+
"os": [
|
| 730 |
+
"win32"
|
| 731 |
+
]
|
| 732 |
+
},
|
| 733 |
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 734 |
+
"version": "4.61.1",
|
| 735 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz",
|
| 736 |
+
"integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==",
|
| 737 |
+
"cpu": [
|
| 738 |
+
"x64"
|
| 739 |
+
],
|
| 740 |
+
"dev": true,
|
| 741 |
+
"license": "MIT",
|
| 742 |
+
"optional": true,
|
| 743 |
+
"os": [
|
| 744 |
+
"win32"
|
| 745 |
+
]
|
| 746 |
+
},
|
| 747 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 748 |
+
"version": "4.61.1",
|
| 749 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz",
|
| 750 |
+
"integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==",
|
| 751 |
+
"cpu": [
|
| 752 |
+
"x64"
|
| 753 |
+
],
|
| 754 |
+
"dev": true,
|
| 755 |
+
"license": "MIT",
|
| 756 |
+
"optional": true,
|
| 757 |
+
"os": [
|
| 758 |
+
"win32"
|
| 759 |
+
]
|
| 760 |
+
},
|
| 761 |
+
"node_modules/@tweenjs/tween.js": {
|
| 762 |
+
"version": "25.0.0",
|
| 763 |
+
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
|
| 764 |
+
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
|
| 765 |
+
"license": "MIT"
|
| 766 |
+
},
|
| 767 |
+
"node_modules/@types/estree": {
|
| 768 |
+
"version": "1.0.9",
|
| 769 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
| 770 |
+
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
| 771 |
+
"dev": true,
|
| 772 |
+
"license": "MIT"
|
| 773 |
+
},
|
| 774 |
+
"node_modules/@types/stats.js": {
|
| 775 |
+
"version": "0.17.4",
|
| 776 |
+
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
| 777 |
+
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
| 778 |
+
"dev": true,
|
| 779 |
+
"license": "MIT"
|
| 780 |
+
},
|
| 781 |
+
"node_modules/@types/three": {
|
| 782 |
+
"version": "0.166.0",
|
| 783 |
+
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.166.0.tgz",
|
| 784 |
+
"integrity": "sha512-FHMnpcdhdbdOOIYbfkTkUVpYMW53odxbTRwd0/xJpYnTzEsjnVnondGAvHZb4z06UW0vo6WPVuvH0/9qrxKx7g==",
|
| 785 |
+
"dev": true,
|
| 786 |
+
"license": "MIT",
|
| 787 |
+
"dependencies": {
|
| 788 |
+
"@tweenjs/tween.js": "~23.1.2",
|
| 789 |
+
"@types/stats.js": "*",
|
| 790 |
+
"@types/webxr": "*",
|
| 791 |
+
"fflate": "~0.8.2",
|
| 792 |
+
"meshoptimizer": "~0.18.1"
|
| 793 |
+
}
|
| 794 |
+
},
|
| 795 |
+
"node_modules/@types/three/node_modules/@tweenjs/tween.js": {
|
| 796 |
+
"version": "23.1.3",
|
| 797 |
+
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
| 798 |
+
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
| 799 |
+
"dev": true,
|
| 800 |
+
"license": "MIT"
|
| 801 |
+
},
|
| 802 |
+
"node_modules/@types/webxr": {
|
| 803 |
+
"version": "0.5.24",
|
| 804 |
+
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
| 805 |
+
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
| 806 |
+
"dev": true,
|
| 807 |
+
"license": "MIT"
|
| 808 |
+
},
|
| 809 |
+
"node_modules/esbuild": {
|
| 810 |
+
"version": "0.21.5",
|
| 811 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
| 812 |
+
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
| 813 |
+
"dev": true,
|
| 814 |
+
"hasInstallScript": true,
|
| 815 |
+
"license": "MIT",
|
| 816 |
+
"bin": {
|
| 817 |
+
"esbuild": "bin/esbuild"
|
| 818 |
+
},
|
| 819 |
+
"engines": {
|
| 820 |
+
"node": ">=12"
|
| 821 |
+
},
|
| 822 |
+
"optionalDependencies": {
|
| 823 |
+
"@esbuild/aix-ppc64": "0.21.5",
|
| 824 |
+
"@esbuild/android-arm": "0.21.5",
|
| 825 |
+
"@esbuild/android-arm64": "0.21.5",
|
| 826 |
+
"@esbuild/android-x64": "0.21.5",
|
| 827 |
+
"@esbuild/darwin-arm64": "0.21.5",
|
| 828 |
+
"@esbuild/darwin-x64": "0.21.5",
|
| 829 |
+
"@esbuild/freebsd-arm64": "0.21.5",
|
| 830 |
+
"@esbuild/freebsd-x64": "0.21.5",
|
| 831 |
+
"@esbuild/linux-arm": "0.21.5",
|
| 832 |
+
"@esbuild/linux-arm64": "0.21.5",
|
| 833 |
+
"@esbuild/linux-ia32": "0.21.5",
|
| 834 |
+
"@esbuild/linux-loong64": "0.21.5",
|
| 835 |
+
"@esbuild/linux-mips64el": "0.21.5",
|
| 836 |
+
"@esbuild/linux-ppc64": "0.21.5",
|
| 837 |
+
"@esbuild/linux-riscv64": "0.21.5",
|
| 838 |
+
"@esbuild/linux-s390x": "0.21.5",
|
| 839 |
+
"@esbuild/linux-x64": "0.21.5",
|
| 840 |
+
"@esbuild/netbsd-x64": "0.21.5",
|
| 841 |
+
"@esbuild/openbsd-x64": "0.21.5",
|
| 842 |
+
"@esbuild/sunos-x64": "0.21.5",
|
| 843 |
+
"@esbuild/win32-arm64": "0.21.5",
|
| 844 |
+
"@esbuild/win32-ia32": "0.21.5",
|
| 845 |
+
"@esbuild/win32-x64": "0.21.5"
|
| 846 |
+
}
|
| 847 |
+
},
|
| 848 |
+
"node_modules/fflate": {
|
| 849 |
+
"version": "0.8.3",
|
| 850 |
+
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
| 851 |
+
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
| 852 |
+
"dev": true,
|
| 853 |
+
"license": "MIT"
|
| 854 |
+
},
|
| 855 |
+
"node_modules/fsevents": {
|
| 856 |
+
"version": "2.3.3",
|
| 857 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 858 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 859 |
+
"dev": true,
|
| 860 |
+
"hasInstallScript": true,
|
| 861 |
+
"license": "MIT",
|
| 862 |
+
"optional": true,
|
| 863 |
+
"os": [
|
| 864 |
+
"darwin"
|
| 865 |
+
],
|
| 866 |
+
"engines": {
|
| 867 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 868 |
+
}
|
| 869 |
+
},
|
| 870 |
+
"node_modules/meshoptimizer": {
|
| 871 |
+
"version": "0.18.1",
|
| 872 |
+
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
|
| 873 |
+
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
|
| 874 |
+
"dev": true,
|
| 875 |
+
"license": "MIT"
|
| 876 |
+
},
|
| 877 |
+
"node_modules/nanoid": {
|
| 878 |
+
"version": "3.3.12",
|
| 879 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
| 880 |
+
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
| 881 |
+
"dev": true,
|
| 882 |
+
"funding": [
|
| 883 |
+
{
|
| 884 |
+
"type": "github",
|
| 885 |
+
"url": "https://github.com/sponsors/ai"
|
| 886 |
+
}
|
| 887 |
+
],
|
| 888 |
+
"license": "MIT",
|
| 889 |
+
"bin": {
|
| 890 |
+
"nanoid": "bin/nanoid.cjs"
|
| 891 |
+
},
|
| 892 |
+
"engines": {
|
| 893 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 894 |
+
}
|
| 895 |
+
},
|
| 896 |
+
"node_modules/picocolors": {
|
| 897 |
+
"version": "1.1.1",
|
| 898 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 899 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 900 |
+
"dev": true,
|
| 901 |
+
"license": "ISC"
|
| 902 |
+
},
|
| 903 |
+
"node_modules/postcss": {
|
| 904 |
+
"version": "8.5.15",
|
| 905 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
| 906 |
+
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
| 907 |
+
"dev": true,
|
| 908 |
+
"funding": [
|
| 909 |
+
{
|
| 910 |
+
"type": "opencollective",
|
| 911 |
+
"url": "https://opencollective.com/postcss/"
|
| 912 |
+
},
|
| 913 |
+
{
|
| 914 |
+
"type": "tidelift",
|
| 915 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 916 |
+
},
|
| 917 |
+
{
|
| 918 |
+
"type": "github",
|
| 919 |
+
"url": "https://github.com/sponsors/ai"
|
| 920 |
+
}
|
| 921 |
+
],
|
| 922 |
+
"license": "MIT",
|
| 923 |
+
"dependencies": {
|
| 924 |
+
"nanoid": "^3.3.12",
|
| 925 |
+
"picocolors": "^1.1.1",
|
| 926 |
+
"source-map-js": "^1.2.1"
|
| 927 |
+
},
|
| 928 |
+
"engines": {
|
| 929 |
+
"node": "^10 || ^12 || >=14"
|
| 930 |
+
}
|
| 931 |
+
},
|
| 932 |
+
"node_modules/rollup": {
|
| 933 |
+
"version": "4.61.1",
|
| 934 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz",
|
| 935 |
+
"integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==",
|
| 936 |
+
"dev": true,
|
| 937 |
+
"license": "MIT",
|
| 938 |
+
"dependencies": {
|
| 939 |
+
"@types/estree": "1.0.9"
|
| 940 |
+
},
|
| 941 |
+
"bin": {
|
| 942 |
+
"rollup": "dist/bin/rollup"
|
| 943 |
+
},
|
| 944 |
+
"engines": {
|
| 945 |
+
"node": ">=18.0.0",
|
| 946 |
+
"npm": ">=8.0.0"
|
| 947 |
+
},
|
| 948 |
+
"optionalDependencies": {
|
| 949 |
+
"@rollup/rollup-android-arm-eabi": "4.61.1",
|
| 950 |
+
"@rollup/rollup-android-arm64": "4.61.1",
|
| 951 |
+
"@rollup/rollup-darwin-arm64": "4.61.1",
|
| 952 |
+
"@rollup/rollup-darwin-x64": "4.61.1",
|
| 953 |
+
"@rollup/rollup-freebsd-arm64": "4.61.1",
|
| 954 |
+
"@rollup/rollup-freebsd-x64": "4.61.1",
|
| 955 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.61.1",
|
| 956 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.61.1",
|
| 957 |
+
"@rollup/rollup-linux-arm64-gnu": "4.61.1",
|
| 958 |
+
"@rollup/rollup-linux-arm64-musl": "4.61.1",
|
| 959 |
+
"@rollup/rollup-linux-loong64-gnu": "4.61.1",
|
| 960 |
+
"@rollup/rollup-linux-loong64-musl": "4.61.1",
|
| 961 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.61.1",
|
| 962 |
+
"@rollup/rollup-linux-ppc64-musl": "4.61.1",
|
| 963 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.61.1",
|
| 964 |
+
"@rollup/rollup-linux-riscv64-musl": "4.61.1",
|
| 965 |
+
"@rollup/rollup-linux-s390x-gnu": "4.61.1",
|
| 966 |
+
"@rollup/rollup-linux-x64-gnu": "4.61.1",
|
| 967 |
+
"@rollup/rollup-linux-x64-musl": "4.61.1",
|
| 968 |
+
"@rollup/rollup-openbsd-x64": "4.61.1",
|
| 969 |
+
"@rollup/rollup-openharmony-arm64": "4.61.1",
|
| 970 |
+
"@rollup/rollup-win32-arm64-msvc": "4.61.1",
|
| 971 |
+
"@rollup/rollup-win32-ia32-msvc": "4.61.1",
|
| 972 |
+
"@rollup/rollup-win32-x64-gnu": "4.61.1",
|
| 973 |
+
"@rollup/rollup-win32-x64-msvc": "4.61.1",
|
| 974 |
+
"fsevents": "~2.3.2"
|
| 975 |
+
}
|
| 976 |
+
},
|
| 977 |
+
"node_modules/source-map-js": {
|
| 978 |
+
"version": "1.2.1",
|
| 979 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 980 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 981 |
+
"dev": true,
|
| 982 |
+
"license": "BSD-3-Clause",
|
| 983 |
+
"engines": {
|
| 984 |
+
"node": ">=0.10.0"
|
| 985 |
+
}
|
| 986 |
+
},
|
| 987 |
+
"node_modules/three": {
|
| 988 |
+
"version": "0.166.1",
|
| 989 |
+
"resolved": "https://registry.npmjs.org/three/-/three-0.166.1.tgz",
|
| 990 |
+
"integrity": "sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg==",
|
| 991 |
+
"license": "MIT"
|
| 992 |
+
},
|
| 993 |
+
"node_modules/typescript": {
|
| 994 |
+
"version": "5.9.3",
|
| 995 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 996 |
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 997 |
+
"dev": true,
|
| 998 |
+
"license": "Apache-2.0",
|
| 999 |
+
"bin": {
|
| 1000 |
+
"tsc": "bin/tsc",
|
| 1001 |
+
"tsserver": "bin/tsserver"
|
| 1002 |
+
},
|
| 1003 |
+
"engines": {
|
| 1004 |
+
"node": ">=14.17"
|
| 1005 |
+
}
|
| 1006 |
+
},
|
| 1007 |
+
"node_modules/vite": {
|
| 1008 |
+
"version": "5.4.21",
|
| 1009 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
| 1010 |
+
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
| 1011 |
+
"dev": true,
|
| 1012 |
+
"license": "MIT",
|
| 1013 |
+
"dependencies": {
|
| 1014 |
+
"esbuild": "^0.21.3",
|
| 1015 |
+
"postcss": "^8.4.43",
|
| 1016 |
+
"rollup": "^4.20.0"
|
| 1017 |
+
},
|
| 1018 |
+
"bin": {
|
| 1019 |
+
"vite": "bin/vite.js"
|
| 1020 |
+
},
|
| 1021 |
+
"engines": {
|
| 1022 |
+
"node": "^18.0.0 || >=20.0.0"
|
| 1023 |
+
},
|
| 1024 |
+
"funding": {
|
| 1025 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 1026 |
+
},
|
| 1027 |
+
"optionalDependencies": {
|
| 1028 |
+
"fsevents": "~2.3.3"
|
| 1029 |
+
},
|
| 1030 |
+
"peerDependencies": {
|
| 1031 |
+
"@types/node": "^18.0.0 || >=20.0.0",
|
| 1032 |
+
"less": "*",
|
| 1033 |
+
"lightningcss": "^1.21.0",
|
| 1034 |
+
"sass": "*",
|
| 1035 |
+
"sass-embedded": "*",
|
| 1036 |
+
"stylus": "*",
|
| 1037 |
+
"sugarss": "*",
|
| 1038 |
+
"terser": "^5.4.0"
|
| 1039 |
+
},
|
| 1040 |
+
"peerDependenciesMeta": {
|
| 1041 |
+
"@types/node": {
|
| 1042 |
+
"optional": true
|
| 1043 |
+
},
|
| 1044 |
+
"less": {
|
| 1045 |
+
"optional": true
|
| 1046 |
+
},
|
| 1047 |
+
"lightningcss": {
|
| 1048 |
+
"optional": true
|
| 1049 |
+
},
|
| 1050 |
+
"sass": {
|
| 1051 |
+
"optional": true
|
| 1052 |
+
},
|
| 1053 |
+
"sass-embedded": {
|
| 1054 |
+
"optional": true
|
| 1055 |
+
},
|
| 1056 |
+
"stylus": {
|
| 1057 |
+
"optional": true
|
| 1058 |
+
},
|
| 1059 |
+
"sugarss": {
|
| 1060 |
+
"optional": true
|
| 1061 |
+
},
|
| 1062 |
+
"terser": {
|
| 1063 |
+
"optional": true
|
| 1064 |
+
}
|
| 1065 |
+
}
|
| 1066 |
+
}
|
| 1067 |
+
}
|
| 1068 |
+
}
|
frontend/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "hf-rubic-instructor-frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.1",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc --noEmit && vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"three": "^0.166.0",
|
| 13 |
+
"@tweenjs/tween.js": "^25.0.0"
|
| 14 |
+
},
|
| 15 |
+
"devDependencies": {
|
| 16 |
+
"@types/three": "^0.166.0",
|
| 17 |
+
"typescript": "^5.4.5",
|
| 18 |
+
"vite": "^5.3.1"
|
| 19 |
+
}
|
| 20 |
+
}
|
frontend/src/README.md
ADDED
|
File without changes
|
frontend/src/configs/debug-config.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// import { GAME_TYPE } from "../../../scene/game-boy-scene/game-boy-games/data/games-config";
|
| 2 |
+
// import { SPACE_INVADERS_SCREEN_TYPE } from "../../../scene/game-boy-scene/game-boy-games/games/space-invaders/data/space-invaders-data";
|
| 3 |
+
// import { TETRIS_SCREEN_TYPE } from "../../../scene/game-boy-scene/game-boy-games/games/tetris/data/tetris-data";
|
| 4 |
+
|
| 5 |
+
const DEBUG_CONFIG = {
|
| 6 |
+
fpsMeter: false,
|
| 7 |
+
rendererStats: false,
|
| 8 |
+
orbitControls: false,
|
| 9 |
+
withoutUIMode: false,
|
| 10 |
+
startState: {
|
| 11 |
+
disableIntro: false,
|
| 12 |
+
zoomIn: false,
|
| 13 |
+
enableGameBoy: false,
|
| 14 |
+
loadGame: null, // GAME_TYPE.Tetris
|
| 15 |
+
// loadGame: GAME_TYPE.SpaceInvaders,
|
| 16 |
+
startScreen: null, // SPACE_INVADERS_SCREEN_TYPE.Round
|
| 17 |
+
// startScreen: TETRIS_SCREEN_TYPE.Gameplay,
|
| 18 |
+
},
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
export default DEBUG_CONFIG;
|
frontend/src/configs/global-light-config.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const GLOBAL_LIGHT_CONFIG = {
|
| 2 |
+
ambient: {
|
| 3 |
+
enabled: false,
|
| 4 |
+
color: 0xFFEFE4,
|
| 5 |
+
intensity: 2,
|
| 6 |
+
},
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export { GLOBAL_LIGHT_CONFIG };
|
frontend/src/configs/scene-config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const SCENE_CONFIG = {
|
| 2 |
+
backgroundColor: 0x999999,
|
| 3 |
+
antialias: false,
|
| 4 |
+
fxaaPass: false,
|
| 5 |
+
maxPixelRatio: 2,
|
| 6 |
+
isMobile: false,
|
| 7 |
+
outlinePass: {
|
| 8 |
+
enabled: true,
|
| 9 |
+
color: '#ffffff',
|
| 10 |
+
edgeGlow: 1,
|
| 11 |
+
edgeStrength: 4,
|
| 12 |
+
edgeThickness: 1,
|
| 13 |
+
pulsePeriod: 4,
|
| 14 |
+
},
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
export default SCENE_CONFIG;
|
frontend/src/configs/sounds-config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const SOUNDS_CONFIG = {
|
| 2 |
+
enabled: true,
|
| 3 |
+
masterVolume: 0.5,
|
| 4 |
+
gameBoyVolume: 0.5,
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export { SOUNDS_CONFIG };
|
frontend/src/core/README.md
ADDED
|
File without changes
|
frontend/src/core/events.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
type Listener<T> = (payload: T) => void;
|
| 2 |
+
|
| 3 |
+
export class Emitter<T> {
|
| 4 |
+
private listeners = new Set<Listener<T>>();
|
| 5 |
+
on(fn: Listener<T>): () => void {
|
| 6 |
+
this.listeners.add(fn);
|
| 7 |
+
return () => this.listeners.delete(fn);
|
| 8 |
+
}
|
| 9 |
+
emit(payload: T): void {
|
| 10 |
+
for (const fn of this.listeners) fn(payload);
|
| 11 |
+
}
|
| 12 |
+
}
|
frontend/src/core/state.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Logical Rubik's cube state, decoupled from Three.js.
|
| 2 |
+
// Faces: U(up), D(down), L(left), R(right), F(front), B(back).
|
| 3 |
+
// Each face is a flat array of 9 sticker colors (same letter as its solved face).
|
| 4 |
+
// Sticker indices, from the viewer's perspective looking at that face:
|
| 5 |
+
// 0 1 2
|
| 6 |
+
// 3 4 5
|
| 7 |
+
// 6 7 8
|
| 8 |
+
|
| 9 |
+
export type FaceKey = 'U' | 'D' | 'L' | 'R' | 'F' | 'B';
|
| 10 |
+
export type Color = FaceKey;
|
| 11 |
+
|
| 12 |
+
export type State = Record<FaceKey, Color[]>;
|
| 13 |
+
|
| 14 |
+
export function solvedState(): State {
|
| 15 |
+
const mk = (c: Color): Color[] => Array(9).fill(c);
|
| 16 |
+
return { U: mk('U'), D: mk('D'), L: mk('L'), R: mk('R'), F: mk('F'), B: mk('B') };
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function rotateFaceCW(face: Color[]): Color[] {
|
| 20 |
+
const f = face;
|
| 21 |
+
return [f[6], f[3], f[0], f[7], f[4], f[1], f[8], f[5], f[2]];
|
| 22 |
+
}
|
| 23 |
+
function rotateFaceCCW(face: Color[]): Color[] {
|
| 24 |
+
const f = face;
|
| 25 |
+
return [f[2], f[5], f[8], f[1], f[4], f[7], f[0], f[3], f[6]];
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// A "cycle" describes which (face, indices) move where for a CW quarter turn of a face layer.
|
| 29 |
+
// Each entry: 4 ordered groups of stickers; each turn shifts group i -> group i+1 (mod 4).
|
| 30 |
+
type Cycle = Array<[FaceKey, number[]]>;
|
| 31 |
+
|
| 32 |
+
const CYCLES: Record<FaceKey, Cycle> = {
|
| 33 |
+
U: [
|
| 34 |
+
['F', [0, 1, 2]],
|
| 35 |
+
['L', [0, 1, 2]],
|
| 36 |
+
['B', [0, 1, 2]],
|
| 37 |
+
['R', [0, 1, 2]]
|
| 38 |
+
],
|
| 39 |
+
D: [
|
| 40 |
+
['F', [6, 7, 8]],
|
| 41 |
+
['R', [6, 7, 8]],
|
| 42 |
+
['B', [6, 7, 8]],
|
| 43 |
+
['L', [6, 7, 8]]
|
| 44 |
+
],
|
| 45 |
+
R: [
|
| 46 |
+
['U', [2, 5, 8]],
|
| 47 |
+
['B', [6, 3, 0]],
|
| 48 |
+
['D', [2, 5, 8]],
|
| 49 |
+
['F', [2, 5, 8]]
|
| 50 |
+
],
|
| 51 |
+
L: [
|
| 52 |
+
['U', [0, 3, 6]],
|
| 53 |
+
['F', [0, 3, 6]],
|
| 54 |
+
['D', [0, 3, 6]],
|
| 55 |
+
['B', [8, 5, 2]]
|
| 56 |
+
],
|
| 57 |
+
F: [
|
| 58 |
+
['U', [6, 7, 8]],
|
| 59 |
+
['R', [0, 3, 6]],
|
| 60 |
+
['D', [2, 1, 0]],
|
| 61 |
+
['L', [8, 5, 2]]
|
| 62 |
+
],
|
| 63 |
+
B: [
|
| 64 |
+
['U', [2, 1, 0]],
|
| 65 |
+
['L', [0, 3, 6]],
|
| 66 |
+
['D', [6, 7, 8]],
|
| 67 |
+
['R', [8, 5, 2]]
|
| 68 |
+
]
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
function applyFaceTurn(state: State, face: FaceKey, prime: boolean): void {
|
| 72 |
+
state[face] = prime ? rotateFaceCCW(state[face]) : rotateFaceCW(state[face]);
|
| 73 |
+
const cycle = CYCLES[face];
|
| 74 |
+
const order = prime ? [3, 2, 1, 0] : [0, 1, 2, 3];
|
| 75 |
+
const groups = order.map(i => cycle[i]);
|
| 76 |
+
const tmp = groups[0][1].map(idx => state[groups[0][0]][idx]);
|
| 77 |
+
for (let i = 0; i < 3; i++) {
|
| 78 |
+
const [fromFace, fromIdx] = groups[i + 1];
|
| 79 |
+
const [toFace, toIdx] = groups[i];
|
| 80 |
+
for (let k = 0; k < 3; k++) state[toFace][toIdx[k]] = state[fromFace][fromIdx[k]];
|
| 81 |
+
}
|
| 82 |
+
const [lastFace, lastIdx] = groups[3];
|
| 83 |
+
for (let k = 0; k < 3; k++) state[lastFace][lastIdx[k]] = tmp[k];
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// Slice and whole-cube rotations are expressed via face turns.
|
| 87 |
+
const COMPOUND: Record<string, string[]> = {
|
| 88 |
+
M: ["L", "R'", "x'"], // middle layer follows L
|
| 89 |
+
"M'": ["L'", "R", "x"],
|
| 90 |
+
E: ["D", "U'", "y'"], // equatorial follows D
|
| 91 |
+
"E'": ["D'", "U", "y"],
|
| 92 |
+
S: ["F'", "B", "z"], // standing follows F
|
| 93 |
+
"S'": ["F", "B'", "z'"],
|
| 94 |
+
// Whole-cube rotations re-color faces (leave logical state shape intact via face permutations).
|
| 95 |
+
// We model x/y/z as cycles of *labels* by applying face turns + relabel; here we approximate via face permutations:
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
// Whole-cube rotation: permute face arrays + rotate each face.
|
| 99 |
+
// x: rotate around R axis (clockwise looking from +X). F->U, U->B, B->D, D->F. L,R rotate.
|
| 100 |
+
function rotX(state: State, prime: boolean): void {
|
| 101 |
+
const s = state;
|
| 102 |
+
if (!prime) {
|
| 103 |
+
const F = s.F, U = s.U, B = s.B, D = s.D;
|
| 104 |
+
s.U = F;
|
| 105 |
+
s.B = rotateFaceCW(rotateFaceCW(U));
|
| 106 |
+
s.D = rotateFaceCW(rotateFaceCW(B));
|
| 107 |
+
s.F = D;
|
| 108 |
+
s.R = rotateFaceCW(s.R);
|
| 109 |
+
s.L = rotateFaceCCW(s.L);
|
| 110 |
+
} else {
|
| 111 |
+
const F = s.F, U = s.U, B = s.B, D = s.D;
|
| 112 |
+
s.F = U;
|
| 113 |
+
s.U = rotateFaceCW(rotateFaceCW(B));
|
| 114 |
+
s.B = rotateFaceCW(rotateFaceCW(D));
|
| 115 |
+
s.D = F;
|
| 116 |
+
s.R = rotateFaceCCW(s.R);
|
| 117 |
+
s.L = rotateFaceCW(s.L);
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
// y: rotate around U axis (clockwise looking from +Y). F->L, L->B, B->R, R->F.
|
| 121 |
+
function rotY(state: State, prime: boolean): void {
|
| 122 |
+
const s = state;
|
| 123 |
+
if (!prime) {
|
| 124 |
+
const F = s.F, L = s.L, B = s.B, R = s.R;
|
| 125 |
+
s.L = F; s.B = L; s.R = B; s.F = R;
|
| 126 |
+
s.U = rotateFaceCW(s.U);
|
| 127 |
+
s.D = rotateFaceCCW(s.D);
|
| 128 |
+
} else {
|
| 129 |
+
const F = s.F, L = s.L, B = s.B, R = s.R;
|
| 130 |
+
s.F = L; s.L = B; s.B = R; s.R = F;
|
| 131 |
+
s.U = rotateFaceCCW(s.U);
|
| 132 |
+
s.D = rotateFaceCW(s.D);
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
// z: rotate around F axis (clockwise looking from +Z). U->R, R->D, D->L, L->U.
|
| 136 |
+
function rotZ(state: State, prime: boolean): void {
|
| 137 |
+
const s = state;
|
| 138 |
+
if (!prime) {
|
| 139 |
+
const U = s.U, R = s.R, D = s.D, L = s.L;
|
| 140 |
+
s.R = rotateFaceCW(U);
|
| 141 |
+
s.D = rotateFaceCW(R);
|
| 142 |
+
s.L = rotateFaceCW(D);
|
| 143 |
+
s.U = rotateFaceCW(L);
|
| 144 |
+
s.F = rotateFaceCW(s.F);
|
| 145 |
+
s.B = rotateFaceCCW(s.B);
|
| 146 |
+
} else {
|
| 147 |
+
const U = s.U, R = s.R, D = s.D, L = s.L;
|
| 148 |
+
s.L = rotateFaceCCW(U);
|
| 149 |
+
s.U = rotateFaceCCW(R);
|
| 150 |
+
s.R = rotateFaceCCW(D);
|
| 151 |
+
s.D = rotateFaceCCW(L);
|
| 152 |
+
s.F = rotateFaceCCW(s.F);
|
| 153 |
+
s.B = rotateFaceCW(s.B);
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
export function applyMove(state: State, move: string): void {
|
| 158 |
+
if (!move) return;
|
| 159 |
+
if (COMPOUND[move]) {
|
| 160 |
+
for (const m of COMPOUND[move]) applyMove(state, m);
|
| 161 |
+
return;
|
| 162 |
+
}
|
| 163 |
+
const prime = move.endsWith("'");
|
| 164 |
+
const base = prime ? move.slice(0, -1) : move;
|
| 165 |
+
if (base === 'x') return rotX(state, prime);
|
| 166 |
+
if (base === 'y') return rotY(state, prime);
|
| 167 |
+
if (base === 'z') return rotZ(state, prime);
|
| 168 |
+
if ('UDLRFB'.includes(base)) return applyFaceTurn(state, base as FaceKey, prime);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
export function isSolved(state: State): boolean {
|
| 172 |
+
return (Object.keys(state) as FaceKey[]).every(f =>
|
| 173 |
+
state[f].every(c => c === state[f][4])
|
| 174 |
+
);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
export function cloneState(s: State): State {
|
| 178 |
+
return {
|
| 179 |
+
U: [...s.U], D: [...s.D], L: [...s.L], R: [...s.R], F: [...s.F], B: [...s.B]
|
| 180 |
+
};
|
| 181 |
+
}
|
frontend/src/main.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import './style.css';
|
| 2 |
+
import { createScene } from './scene/scene';
|
| 3 |
+
import { CubeMesh } from './scene/cube/cube';
|
| 4 |
+
import { MoveAnimator } from './scene/cube/animator';
|
| 5 |
+
import { attachKeyboard } from './ui/controls/keyboard';
|
| 6 |
+
import { attachDragControls } from './ui/controls/drag-controls';
|
| 7 |
+
import { DebuggerPanel } from './ui/debugger';
|
| 8 |
+
import { applyMove, solvedState, type State } from './core/state';
|
| 9 |
+
import DEBUG_CONFIG from './configs/debug-config';
|
| 10 |
+
|
| 11 |
+
const app = document.getElementById('app')!;
|
| 12 |
+
const ctx = createScene(app);
|
| 13 |
+
|
| 14 |
+
const cube = new CubeMesh();
|
| 15 |
+
ctx.scene.add(cube.root);
|
| 16 |
+
|
| 17 |
+
const animator = new MoveAnimator(cube, cube.root);
|
| 18 |
+
|
| 19 |
+
let state: State = solvedState();
|
| 20 |
+
|
| 21 |
+
const debuggerPanel = !DEBUG_CONFIG.withoutUIMode ? new DebuggerPanel(document.body) : null;
|
| 22 |
+
debuggerPanel?.render(state);
|
| 23 |
+
|
| 24 |
+
animator.onMoveComplete = (name) => {
|
| 25 |
+
applyMove(state, name);
|
| 26 |
+
debuggerPanel?.pushMove(name);
|
| 27 |
+
debuggerPanel?.render(state);
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
function resetCube(): void {
|
| 31 |
+
if (animator.isBusy()) return;
|
| 32 |
+
cube.root.clear();
|
| 33 |
+
cube.cubies.length = 0;
|
| 34 |
+
const fresh = new CubeMesh();
|
| 35 |
+
for (const c of fresh.cubies) {
|
| 36 |
+
cube.root.add(c.mesh);
|
| 37 |
+
cube.cubies.push(c);
|
| 38 |
+
}
|
| 39 |
+
state = solvedState();
|
| 40 |
+
debuggerPanel?.reset();
|
| 41 |
+
debuggerPanel?.render(state);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
attachKeyboard(animator, {
|
| 45 |
+
onReset: resetCube
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
attachDragControls(cube, animator, ctx.camera, ctx.renderer.domElement);
|
| 49 |
+
|
| 50 |
+
renderHelp();
|
| 51 |
+
|
| 52 |
+
function tick(now: number): void {
|
| 53 |
+
animator.update(now);
|
| 54 |
+
ctx.controls?.update();
|
| 55 |
+
ctx.renderer.render(ctx.scene, ctx.camera);
|
| 56 |
+
requestAnimationFrame(tick);
|
| 57 |
+
}
|
| 58 |
+
requestAnimationFrame(tick);
|
| 59 |
+
|
| 60 |
+
function renderHelp(): void {
|
| 61 |
+
if (DEBUG_CONFIG.withoutUIMode) return;
|
| 62 |
+
const help = document.createElement('div');
|
| 63 |
+
help.id = 'help';
|
| 64 |
+
help.innerHTML = `
|
| 65 |
+
<div><b>Drag</b> a face to rotate that layer</div>
|
| 66 |
+
<div><b>U/D/L/R/F/B</b> face turns <b>M/E/S</b> middle slices <b>X/Y/Z</b> whole cube</div>
|
| 67 |
+
<div><b>Shift</b>+key = prime (counter-clockwise) <b>Space</b> = scramble <b>Enter</b> = reset</div>
|
| 68 |
+
`;
|
| 69 |
+
document.body.appendChild(help);
|
| 70 |
+
}
|
frontend/src/scene/README.md
ADDED
|
File without changes
|
frontend/src/scene/cube/animator.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as THREE from 'three';
|
| 2 |
+
import { CubeMesh, type Axis, type Cubie } from './cube';
|
| 3 |
+
|
| 4 |
+
export interface MoveSpec {
|
| 5 |
+
axis: Axis;
|
| 6 |
+
// Which logical slices (-1, 0, 1) this move rotates.
|
| 7 |
+
slices: number[];
|
| 8 |
+
// Direction multiplier: +1 = clockwise looking from +axis toward origin.
|
| 9 |
+
// For face moves, signs are chosen so move name without prime matches standard CW-from-outside.
|
| 10 |
+
dir: 1 | -1;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
// Mapping for base moves (no prime). Prime is handled by the caller (multiply dir by -1).
|
| 14 |
+
const BASE_MOVES: Record<string, MoveSpec> = {
|
| 15 |
+
// Face turns. CW looking from outside the named face.
|
| 16 |
+
// U: top layer, viewed from +Y => CW around +Y => dir +1.
|
| 17 |
+
U: { axis: 'y', slices: [1], dir: 1 },
|
| 18 |
+
D: { axis: 'y', slices: [-1], dir: -1 },
|
| 19 |
+
R: { axis: 'x', slices: [1], dir: 1 },
|
| 20 |
+
L: { axis: 'x', slices: [-1], dir: -1 },
|
| 21 |
+
F: { axis: 'z', slices: [1], dir: 1 },
|
| 22 |
+
B: { axis: 'z', slices: [-1], dir: -1 },
|
| 23 |
+
// Slice turns: middle layer. M follows L (dir matches L), E follows D, S follows F.
|
| 24 |
+
M: { axis: 'x', slices: [0], dir: -1 },
|
| 25 |
+
E: { axis: 'y', slices: [0], dir: -1 },
|
| 26 |
+
S: { axis: 'z', slices: [0], dir: 1 },
|
| 27 |
+
// Whole-cube rotations.
|
| 28 |
+
x: { axis: 'x', slices: [-1, 0, 1], dir: 1 },
|
| 29 |
+
y: { axis: 'y', slices: [-1, 0, 1], dir: 1 },
|
| 30 |
+
z: { axis: 'z', slices: [-1, 0, 1], dir: 1 }
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
export function parseMove(move: string): MoveSpec | null {
|
| 34 |
+
const prime = move.endsWith("'");
|
| 35 |
+
const base = prime ? move.slice(0, -1) : move;
|
| 36 |
+
const spec = BASE_MOVES[base];
|
| 37 |
+
if (!spec) return null;
|
| 38 |
+
return { axis: spec.axis, slices: spec.slices, dir: (prime ? -spec.dir : spec.dir) as 1 | -1 };
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const AXIS_VEC: Record<Axis, THREE.Vector3> = {
|
| 42 |
+
x: new THREE.Vector3(1, 0, 0),
|
| 43 |
+
y: new THREE.Vector3(0, 1, 0),
|
| 44 |
+
z: new THREE.Vector3(0, 0, 1)
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
// Rotate logical lattice coords by a quarter turn around `axis` with `dir`.
|
| 48 |
+
// Assumes coords are integer (-1,0,1).
|
| 49 |
+
function rotateCoord(coord: THREE.Vector3, axis: Axis, dir: 1 | -1): void {
|
| 50 |
+
const c = coord;
|
| 51 |
+
if (axis === 'x') {
|
| 52 |
+
const y = c.y, z = c.z;
|
| 53 |
+
if (dir === 1) { c.y = -z; c.z = y; }
|
| 54 |
+
else { c.y = z; c.z = -y; }
|
| 55 |
+
} else if (axis === 'y') {
|
| 56 |
+
const x = c.x, z = c.z;
|
| 57 |
+
if (dir === 1) { c.x = z; c.z = -x; }
|
| 58 |
+
else { c.x = -z; c.z = x; }
|
| 59 |
+
} else {
|
| 60 |
+
const x = c.x, y = c.y;
|
| 61 |
+
if (dir === 1) { c.x = -y; c.y = x; }
|
| 62 |
+
else { c.x = y; c.y = -x; }
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
export interface MoveJob {
|
| 67 |
+
name: string;
|
| 68 |
+
spec: MoveSpec;
|
| 69 |
+
affected: Cubie[];
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
export function buildMoveJob(cube: CubeMesh, name: string): MoveJob | null {
|
| 73 |
+
const spec = parseMove(name);
|
| 74 |
+
if (!spec) return null;
|
| 75 |
+
const affected: Cubie[] = [];
|
| 76 |
+
for (const slice of spec.slices) affected.push(...cube.cubiesOnLayer(spec.axis, slice));
|
| 77 |
+
return { name, spec, affected };
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Animator: keeps a FIFO queue of moves. Each move:
|
| 81 |
+
// 1. Reparents affected cubies under a temporary pivot Group (preserving world transforms).
|
| 82 |
+
// 2. Tweens pivot.rotation by ±π/2 around the axis.
|
| 83 |
+
// 3. On complete: bakes pivot transform into each cubie, reparents back to cube.root,
|
| 84 |
+
// and updates each cubie's logical lattice coord.
|
| 85 |
+
|
| 86 |
+
const QUARTER = Math.PI / 2;
|
| 87 |
+
|
| 88 |
+
interface ActiveMove {
|
| 89 |
+
job: MoveJob;
|
| 90 |
+
pivot: THREE.Group;
|
| 91 |
+
// Cached pre-tween transforms (in cube.root frame), so we can restore exactly on finish
|
| 92 |
+
// and then apply the analytic quarter-turn — avoids float drift from attach()/detach().
|
| 93 |
+
preState: { cubie: Cubie; position: THREE.Vector3; quaternion: THREE.Quaternion }[];
|
| 94 |
+
start: number;
|
| 95 |
+
to: number;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
export class MoveAnimator {
|
| 99 |
+
private queue: MoveJob[] = [];
|
| 100 |
+
private current: ActiveMove | null = null;
|
| 101 |
+
readonly durationMs: number;
|
| 102 |
+
onMoveStart?: (name: string) => void;
|
| 103 |
+
onMoveComplete?: (name: string) => void;
|
| 104 |
+
|
| 105 |
+
constructor(private cube: CubeMesh, private parent: THREE.Object3D, durationMs = 220) {
|
| 106 |
+
this.durationMs = durationMs;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
enqueue(name: string): boolean {
|
| 110 |
+
const job = buildMoveJob(this.cube, name);
|
| 111 |
+
if (!job) return false;
|
| 112 |
+
this.queue.push(job);
|
| 113 |
+
return true;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
enqueueMany(moves: string[]): void {
|
| 117 |
+
for (const m of moves) this.enqueue(m);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
isBusy(): boolean { return this.current !== null || this.queue.length > 0; }
|
| 121 |
+
|
| 122 |
+
update(now: number): void {
|
| 123 |
+
if (!this.current && this.queue.length > 0) this.beginNext(now);
|
| 124 |
+
if (!this.current) return;
|
| 125 |
+
const c = this.current;
|
| 126 |
+
const t = Math.min(1, (now - c.start) / this.durationMs);
|
| 127 |
+
const eased = easeInOutCubic(t);
|
| 128 |
+
c.pivot.rotation[c.job.spec.axis] = c.to * eased;
|
| 129 |
+
if (t >= 1) this.finishCurrent();
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
private beginNext(now: number): void {
|
| 133 |
+
const job = this.queue.shift()!;
|
| 134 |
+
const pivot = new THREE.Group();
|
| 135 |
+
this.parent.add(pivot);
|
| 136 |
+
const preState = job.affected.map(cubie => ({
|
| 137 |
+
cubie,
|
| 138 |
+
position: cubie.mesh.position.clone(),
|
| 139 |
+
quaternion: cubie.mesh.quaternion.clone()
|
| 140 |
+
}));
|
| 141 |
+
for (const cubie of job.affected) pivot.attach(cubie.mesh);
|
| 142 |
+
this.current = { job, pivot, preState, start: now, to: job.spec.dir * QUARTER };
|
| 143 |
+
this.onMoveStart?.(job.name);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
private finishCurrent(): void {
|
| 147 |
+
const c = this.current!;
|
| 148 |
+
const axis = AXIS_VEC[c.job.spec.axis];
|
| 149 |
+
const turn = new THREE.Quaternion().setFromAxisAngle(axis, c.to);
|
| 150 |
+
|
| 151 |
+
for (const entry of c.preState) {
|
| 152 |
+
// Reparent back to root and apply analytic quarter-turn to the pre-tween transform.
|
| 153 |
+
this.parent.add(entry.cubie.mesh);
|
| 154 |
+
const newPos = entry.position.clone().applyQuaternion(turn);
|
| 155 |
+
const newQuat = turn.clone().multiply(entry.quaternion);
|
| 156 |
+
entry.cubie.mesh.position.copy(newPos);
|
| 157 |
+
entry.cubie.mesh.quaternion.copy(newQuat);
|
| 158 |
+
rotateCoord(entry.cubie.coord, c.job.spec.axis, c.job.spec.dir);
|
| 159 |
+
}
|
| 160 |
+
this.parent.remove(c.pivot);
|
| 161 |
+
const finishedName = c.job.name;
|
| 162 |
+
this.current = null;
|
| 163 |
+
this.onMoveComplete?.(finishedName);
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
function easeInOutCubic(t: number): number {
|
| 168 |
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// Utility kept for tests/debug.
|
| 172 |
+
export const _internal = { rotateCoord };
|
frontend/src/scene/cube/cube.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as THREE from 'three';
|
| 2 |
+
import type { FaceKey } from '../../core/state';
|
| 3 |
+
|
| 4 |
+
// Standard Rubik's color scheme.
|
| 5 |
+
export const FACE_COLORS: Record<FaceKey, number> = {
|
| 6 |
+
U: 0xffffff, // white
|
| 7 |
+
D: 0xffd500, // yellow
|
| 8 |
+
L: 0xff8c00, // orange
|
| 9 |
+
R: 0xc41e3a, // red
|
| 10 |
+
F: 0x009e60, // green
|
| 11 |
+
B: 0x0051ba // blue
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
export const CUBIE_SIZE = 1;
|
| 15 |
+
export const CUBIE_GAP = 0.02;
|
| 16 |
+
|
| 17 |
+
const STICKER_INSET = 0.06;
|
| 18 |
+
const STICKER_DEPTH = 0.51;
|
| 19 |
+
|
| 20 |
+
export type Axis = 'x' | 'y' | 'z';
|
| 21 |
+
|
| 22 |
+
export interface Cubie {
|
| 23 |
+
mesh: THREE.Object3D;
|
| 24 |
+
// Logical lattice coordinates in {-1,0,1}^3 (updated after each move).
|
| 25 |
+
coord: THREE.Vector3;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export class CubeMesh {
|
| 29 |
+
readonly root: THREE.Group;
|
| 30 |
+
readonly cubies: Cubie[] = [];
|
| 31 |
+
|
| 32 |
+
constructor() {
|
| 33 |
+
this.root = new THREE.Group();
|
| 34 |
+
const step = CUBIE_SIZE + CUBIE_GAP;
|
| 35 |
+
|
| 36 |
+
const blackMat = new THREE.MeshBasicMaterial({ color: 0x111111 });
|
| 37 |
+
const geom = new THREE.BoxGeometry(CUBIE_SIZE, CUBIE_SIZE, CUBIE_SIZE);
|
| 38 |
+
|
| 39 |
+
for (let x = -1; x <= 1; x++) {
|
| 40 |
+
for (let y = -1; y <= 1; y++) {
|
| 41 |
+
for (let z = -1; z <= 1; z++) {
|
| 42 |
+
const cubie = new THREE.Group();
|
| 43 |
+
const body = new THREE.Mesh(geom, blackMat);
|
| 44 |
+
cubie.add(body);
|
| 45 |
+
|
| 46 |
+
if (x === 1) addSticker(cubie, 'R', 'x', 1);
|
| 47 |
+
if (x === -1) addSticker(cubie, 'L', 'x', -1);
|
| 48 |
+
if (y === 1) addSticker(cubie, 'U', 'y', 1);
|
| 49 |
+
if (y === -1) addSticker(cubie, 'D', 'y', -1);
|
| 50 |
+
if (z === 1) addSticker(cubie, 'F', 'z', 1);
|
| 51 |
+
if (z === -1) addSticker(cubie, 'B', 'z', -1);
|
| 52 |
+
|
| 53 |
+
cubie.position.set(x * step, y * step, z * step);
|
| 54 |
+
this.root.add(cubie);
|
| 55 |
+
this.cubies.push({ mesh: cubie, coord: new THREE.Vector3(x, y, z) });
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
cubiesOnLayer(axis: Axis, slice: number): Cubie[] {
|
| 62 |
+
return this.cubies.filter(c => Math.round(c.coord[axis]) === slice);
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
function addSticker(parent: THREE.Object3D, face: FaceKey, axis: Axis, sign: 1 | -1) {
|
| 67 |
+
const size = CUBIE_SIZE - STICKER_INSET * 2;
|
| 68 |
+
const plane = new THREE.PlaneGeometry(size, size);
|
| 69 |
+
const mat = new THREE.MeshBasicMaterial({ color: FACE_COLORS[face], side: THREE.DoubleSide });
|
| 70 |
+
const mesh = new THREE.Mesh(plane, mat);
|
| 71 |
+
mesh.userData.face = face;
|
| 72 |
+
mesh.userData.isSticker = true;
|
| 73 |
+
if (axis === 'x') {
|
| 74 |
+
mesh.rotation.y = sign === 1 ? Math.PI / 2 : -Math.PI / 2;
|
| 75 |
+
mesh.position.x = sign * STICKER_DEPTH;
|
| 76 |
+
} else if (axis === 'y') {
|
| 77 |
+
mesh.rotation.x = sign === 1 ? -Math.PI / 2 : Math.PI / 2;
|
| 78 |
+
mesh.position.y = sign * STICKER_DEPTH;
|
| 79 |
+
} else {
|
| 80 |
+
if (sign === -1) mesh.rotation.y = Math.PI;
|
| 81 |
+
mesh.position.z = sign * STICKER_DEPTH;
|
| 82 |
+
}
|
| 83 |
+
parent.add(mesh);
|
| 84 |
+
}
|
frontend/src/scene/cube/scramble.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const FACES = ['U', 'D', 'L', 'R', 'F', 'B'];
|
| 2 |
+
const SUFFIXES = ['', "'", '2'];
|
| 3 |
+
|
| 4 |
+
// 2 means double turn (we expand to two quarter turns since the animator handles only quarters).
|
| 5 |
+
function expand(move: string): string[] {
|
| 6 |
+
if (move.endsWith('2')) {
|
| 7 |
+
const base = move.slice(0, -1);
|
| 8 |
+
return [base, base];
|
| 9 |
+
}
|
| 10 |
+
return [move];
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function generateScramble(length = 20): string[] {
|
| 14 |
+
const moves: string[] = [];
|
| 15 |
+
let lastFace = '';
|
| 16 |
+
let beforeLastFace = '';
|
| 17 |
+
while (moves.length < length) {
|
| 18 |
+
const face = FACES[Math.floor(Math.random() * FACES.length)];
|
| 19 |
+
if (face === lastFace) continue;
|
| 20 |
+
if (face === beforeLastFace && areOpposite(face, lastFace)) continue;
|
| 21 |
+
const suf = SUFFIXES[Math.floor(Math.random() * SUFFIXES.length)];
|
| 22 |
+
moves.push(face + suf);
|
| 23 |
+
beforeLastFace = lastFace;
|
| 24 |
+
lastFace = face;
|
| 25 |
+
}
|
| 26 |
+
return moves.flatMap(expand);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
function areOpposite(a: string, b: string): boolean {
|
| 30 |
+
const pairs: Record<string, string> = { U: 'D', D: 'U', L: 'R', R: 'L', F: 'B', B: 'F' };
|
| 31 |
+
return pairs[a] === b;
|
| 32 |
+
}
|
frontend/src/scene/scene.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as THREE from 'three';
|
| 2 |
+
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
| 3 |
+
import SCENE_CONFIG from '../configs/scene-config';
|
| 4 |
+
import DEBUG_CONFIG from '../configs/debug-config';
|
| 5 |
+
|
| 6 |
+
export interface SceneCtx {
|
| 7 |
+
scene: THREE.Scene;
|
| 8 |
+
camera: THREE.PerspectiveCamera;
|
| 9 |
+
renderer: THREE.WebGLRenderer;
|
| 10 |
+
controls: OrbitControls | null;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function createScene(container: HTMLElement): SceneCtx {
|
| 14 |
+
const scene = new THREE.Scene();
|
| 15 |
+
scene.background = new THREE.Color(SCENE_CONFIG.backgroundColor);
|
| 16 |
+
|
| 17 |
+
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
|
| 18 |
+
camera.position.set(5, 5, 7);
|
| 19 |
+
camera.lookAt(0, 0, 0);
|
| 20 |
+
|
| 21 |
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 22 |
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, SCENE_CONFIG.maxPixelRatio));
|
| 23 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 24 |
+
renderer.domElement.classList.add('threejs-canvas');
|
| 25 |
+
container.appendChild(renderer.domElement);
|
| 26 |
+
|
| 27 |
+
let controls: OrbitControls | null = null;
|
| 28 |
+
if (DEBUG_CONFIG.orbitControls) {
|
| 29 |
+
controls = new OrbitControls(camera, renderer.domElement);
|
| 30 |
+
controls.enableDamping = true;
|
| 31 |
+
controls.enablePan = false;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
window.addEventListener('resize', () => {
|
| 35 |
+
camera.aspect = window.innerWidth / window.innerHeight;
|
| 36 |
+
camera.updateProjectionMatrix();
|
| 37 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
return { scene, camera, renderer, controls };
|
| 41 |
+
}
|
frontend/src/style.css
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.threejs-canvas
|
| 2 |
+
{
|
| 3 |
+
position: fixed;
|
| 4 |
+
top: 0;
|
| 5 |
+
left: 0;
|
| 6 |
+
outline: none;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.pixi-canvas
|
| 10 |
+
{
|
| 11 |
+
position: fixed;
|
| 12 |
+
top: 0;
|
| 13 |
+
left: 0;
|
| 14 |
+
outline: none;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
html, body {
|
| 18 |
+
margin: 0;
|
| 19 |
+
padding: 0;
|
| 20 |
+
height: 100%;
|
| 21 |
+
background: #999;
|
| 22 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 23 |
+
color: #111;
|
| 24 |
+
overflow: hidden;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
#app { width: 100vw; height: 100vh; }
|
| 28 |
+
|
| 29 |
+
#debugger {
|
| 30 |
+
position: fixed;
|
| 31 |
+
top: 12px;
|
| 32 |
+
right: 12px;
|
| 33 |
+
background: rgba(20, 20, 20, 0.78);
|
| 34 |
+
color: #eee;
|
| 35 |
+
padding: 12px 14px;
|
| 36 |
+
border-radius: 8px;
|
| 37 |
+
font-size: 12px;
|
| 38 |
+
line-height: 1.4;
|
| 39 |
+
min-width: 220px;
|
| 40 |
+
max-width: 280px;
|
| 41 |
+
box-shadow: 0 6px 20px rgba(0,0,0,.35);
|
| 42 |
+
user-select: none;
|
| 43 |
+
z-index: 10;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
#debugger h3 {
|
| 47 |
+
margin: 0 0 6px 0;
|
| 48 |
+
font-size: 12px;
|
| 49 |
+
letter-spacing: .08em;
|
| 50 |
+
text-transform: uppercase;
|
| 51 |
+
color: #9cf;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
#debugger .row { display: flex; justify-content: space-between; gap: 8px; }
|
| 55 |
+
#debugger .solved-yes { color: #6f6; }
|
| 56 |
+
#debugger .solved-no { color: #f88; }
|
| 57 |
+
|
| 58 |
+
#debugger .faces {
|
| 59 |
+
display: grid;
|
| 60 |
+
grid-template-columns: repeat(4, auto);
|
| 61 |
+
gap: 6px;
|
| 62 |
+
margin: 8px 0;
|
| 63 |
+
}
|
| 64 |
+
#debugger .face {
|
| 65 |
+
display: grid;
|
| 66 |
+
grid-template-columns: repeat(3, 10px);
|
| 67 |
+
grid-template-rows: repeat(3, 10px);
|
| 68 |
+
gap: 1px;
|
| 69 |
+
padding: 2px;
|
| 70 |
+
background: #000;
|
| 71 |
+
border-radius: 2px;
|
| 72 |
+
}
|
| 73 |
+
#debugger .face .label {
|
| 74 |
+
grid-column: 1 / span 3;
|
| 75 |
+
text-align: center;
|
| 76 |
+
font-size: 9px;
|
| 77 |
+
color: #aaa;
|
| 78 |
+
margin-bottom: 1px;
|
| 79 |
+
}
|
| 80 |
+
#debugger .sticker { width: 10px; height: 10px; border-radius: 1px; }
|
| 81 |
+
|
| 82 |
+
#debugger .history {
|
| 83 |
+
max-height: 80px;
|
| 84 |
+
overflow-y: auto;
|
| 85 |
+
font-size: 11px;
|
| 86 |
+
background: rgba(0,0,0,0.35);
|
| 87 |
+
padding: 4px 6px;
|
| 88 |
+
border-radius: 4px;
|
| 89 |
+
white-space: normal;
|
| 90 |
+
word-break: break-word;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
#help {
|
| 94 |
+
position: fixed;
|
| 95 |
+
bottom: 12px;
|
| 96 |
+
left: 12px;
|
| 97 |
+
background: rgba(20,20,20,0.78);
|
| 98 |
+
color: #ddd;
|
| 99 |
+
padding: 8px 12px;
|
| 100 |
+
border-radius: 6px;
|
| 101 |
+
font-size: 11px;
|
| 102 |
+
line-height: 1.5;
|
| 103 |
+
z-index: 10;
|
| 104 |
+
}
|
| 105 |
+
#help b { color: #9cf; }
|
frontend/src/ui/README.md
ADDED
|
File without changes
|
frontend/src/ui/controls/drag-controls.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as THREE from 'three';
|
| 2 |
+
import type { CubeMesh, Cubie, Axis } from '../../scene/cube/cube';
|
| 3 |
+
import type { MoveAnimator } from '../../scene/cube/animator';
|
| 4 |
+
import type { FaceKey } from '../../core/state';
|
| 5 |
+
|
| 6 |
+
interface PendingDrag {
|
| 7 |
+
startScreen: THREE.Vector2;
|
| 8 |
+
hitFace: FaceKey;
|
| 9 |
+
hitCubie: Cubie;
|
| 10 |
+
hitWorldPoint: THREE.Vector3;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export interface DragOptions {
|
| 14 |
+
onMove?: (move: string) => void;
|
| 15 |
+
// Pixels of drag below which we ignore the gesture.
|
| 16 |
+
threshold?: number;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export function attachDragControls(
|
| 20 |
+
cube: CubeMesh,
|
| 21 |
+
animator: MoveAnimator,
|
| 22 |
+
camera: THREE.Camera,
|
| 23 |
+
domEl: HTMLElement,
|
| 24 |
+
opts: DragOptions = {}
|
| 25 |
+
): () => void {
|
| 26 |
+
const threshold = opts.threshold ?? 14;
|
| 27 |
+
const raycaster = new THREE.Raycaster();
|
| 28 |
+
const ndc = new THREE.Vector2();
|
| 29 |
+
let pending: PendingDrag | null = null;
|
| 30 |
+
|
| 31 |
+
function ndcFromEvent(ev: PointerEvent): void {
|
| 32 |
+
const rect = domEl.getBoundingClientRect();
|
| 33 |
+
ndc.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
|
| 34 |
+
ndc.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function findStickerHit(): { face: FaceKey; point: THREE.Vector3; cubie: Cubie } | null {
|
| 38 |
+
raycaster.setFromCamera(ndc, camera);
|
| 39 |
+
// Collect sticker meshes for raycasting.
|
| 40 |
+
const stickers: THREE.Object3D[] = [];
|
| 41 |
+
for (const c of cube.cubies) {
|
| 42 |
+
c.mesh.traverse(child => {
|
| 43 |
+
if ((child as any).userData?.isSticker) stickers.push(child);
|
| 44 |
+
});
|
| 45 |
+
}
|
| 46 |
+
const hits = raycaster.intersectObjects(stickers, false);
|
| 47 |
+
if (!hits.length) return null;
|
| 48 |
+
const hit = hits[0];
|
| 49 |
+
const face = hit.object.userData.face as FaceKey;
|
| 50 |
+
const cubie = findOwningCubie(cube, hit.object);
|
| 51 |
+
if (!cubie) return null;
|
| 52 |
+
return { face, point: hit.point.clone(), cubie };
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function onPointerDown(ev: PointerEvent): void {
|
| 56 |
+
if (animator.isBusy()) return;
|
| 57 |
+
ndcFromEvent(ev);
|
| 58 |
+
const hit = findStickerHit();
|
| 59 |
+
if (!hit) return;
|
| 60 |
+
pending = {
|
| 61 |
+
startScreen: new THREE.Vector2(ev.clientX, ev.clientY),
|
| 62 |
+
hitFace: hit.face,
|
| 63 |
+
hitCubie: hit.cubie,
|
| 64 |
+
hitWorldPoint: hit.point
|
| 65 |
+
};
|
| 66 |
+
(ev.target as Element).setPointerCapture?.(ev.pointerId);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
function onPointerUp(ev: PointerEvent): void {
|
| 70 |
+
if (!pending) return;
|
| 71 |
+
const dx = ev.clientX - pending.startScreen.x;
|
| 72 |
+
const dy = ev.clientY - pending.startScreen.y;
|
| 73 |
+
if (Math.hypot(dx, dy) < threshold) { pending = null; return; }
|
| 74 |
+
|
| 75 |
+
const move = resolveDragToMove(camera, pending, dx, dy);
|
| 76 |
+
pending = null;
|
| 77 |
+
if (move && animator.enqueue(move)) opts.onMove?.(move);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function onPointerCancel(): void { pending = null; }
|
| 81 |
+
|
| 82 |
+
domEl.addEventListener('pointerdown', onPointerDown);
|
| 83 |
+
window.addEventListener('pointerup', onPointerUp);
|
| 84 |
+
window.addEventListener('pointercancel', onPointerCancel);
|
| 85 |
+
return () => {
|
| 86 |
+
domEl.removeEventListener('pointerdown', onPointerDown);
|
| 87 |
+
window.removeEventListener('pointerup', onPointerUp);
|
| 88 |
+
window.removeEventListener('pointercancel', onPointerCancel);
|
| 89 |
+
};
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
function findOwningCubie(cube: CubeMesh, sticker: THREE.Object3D): Cubie | null {
|
| 93 |
+
let n: THREE.Object3D | null = sticker;
|
| 94 |
+
while (n) {
|
| 95 |
+
const found = cube.cubies.find(c => c.mesh === n);
|
| 96 |
+
if (found) return found;
|
| 97 |
+
n = n.parent;
|
| 98 |
+
}
|
| 99 |
+
return null;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// Standard normal vectors for each face in cube-local (root) coords.
|
| 103 |
+
const FACE_NORMAL: Record<FaceKey, THREE.Vector3> = {
|
| 104 |
+
R: new THREE.Vector3(1, 0, 0),
|
| 105 |
+
L: new THREE.Vector3(-1, 0, 0),
|
| 106 |
+
U: new THREE.Vector3(0, 1, 0),
|
| 107 |
+
D: new THREE.Vector3(0, -1, 0),
|
| 108 |
+
F: new THREE.Vector3(0, 0, 1),
|
| 109 |
+
B: new THREE.Vector3(0, 0, -1)
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
// In-plane axes for each face: two perpendicular unit vectors lying in the face plane.
|
| 113 |
+
// Each entry maps a face to two rotation axes that quarter-turns of a layer along this face can pivot around.
|
| 114 |
+
const FACE_PLANE_AXES: Record<FaceKey, [THREE.Vector3, THREE.Vector3]> = {
|
| 115 |
+
U: [new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 1)],
|
| 116 |
+
D: [new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 0, 1)],
|
| 117 |
+
R: [new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1)],
|
| 118 |
+
L: [new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, 0, 1)],
|
| 119 |
+
F: [new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0)],
|
| 120 |
+
B: [new THREE.Vector3(1, 0, 0), new THREE.Vector3(0, 1, 0)]
|
| 121 |
+
};
|
| 122 |
+
|
| 123 |
+
function resolveDragToMove(
|
| 124 |
+
camera: THREE.Camera,
|
| 125 |
+
pending: PendingDrag,
|
| 126 |
+
dx: number,
|
| 127 |
+
dy: number
|
| 128 |
+
): string | null {
|
| 129 |
+
// Project the two in-plane axes from the hit point into screen space; pick the one whose
|
| 130 |
+
// screen direction better aligns with the drag vector. The cross product of that axis with
|
| 131 |
+
// the face normal selects the rotation axis of the layer being swiped, and the sign tells us
|
| 132 |
+
// which direction.
|
| 133 |
+
const screenDrag = new THREE.Vector2(dx, -dy); // y inverted for screen space
|
| 134 |
+
const [a1, a2] = FACE_PLANE_AXES[pending.hitFace];
|
| 135 |
+
const s1 = projectDirToScreen(camera, pending.hitWorldPoint, a1);
|
| 136 |
+
const s2 = projectDirToScreen(camera, pending.hitWorldPoint, a2);
|
| 137 |
+
const dot1 = Math.abs(s1.dot(screenDrag));
|
| 138 |
+
const dot2 = Math.abs(s2.dot(screenDrag));
|
| 139 |
+
const dragAxis = dot1 >= dot2 ? a1 : a2;
|
| 140 |
+
const dragScreen = dot1 >= dot2 ? s1 : s2;
|
| 141 |
+
const sign = Math.sign(dragScreen.dot(screenDrag)) as 1 | -1 | 0;
|
| 142 |
+
if (sign === 0) return null;
|
| 143 |
+
|
| 144 |
+
// The swiped layer rotates around the axis perpendicular to (face normal, dragAxis):
|
| 145 |
+
// rotAxis = faceNormal × dragAxis.
|
| 146 |
+
const faceNormal = FACE_NORMAL[pending.hitFace];
|
| 147 |
+
const rotAxisVec = new THREE.Vector3().crossVectors(faceNormal, dragAxis).normalize();
|
| 148 |
+
const rotAxis = dominantAxis(rotAxisVec);
|
| 149 |
+
if (!rotAxis) return null;
|
| 150 |
+
const rotAxisSign = (rotAxisVec[rotAxis] >= 0 ? 1 : -1) as 1 | -1;
|
| 151 |
+
|
| 152 |
+
// The slice is the cubie's coord on this rotation axis.
|
| 153 |
+
const slice = Math.round(pending.hitCubie.coord[rotAxis]);
|
| 154 |
+
|
| 155 |
+
// Build the move name from (rotAxis, slice). Direction = positive screen drag along dragAxis
|
| 156 |
+
// means rotation by +sign * rotAxisSign around rotAxisVec, i.e., dir = +sign * rotAxisSign.
|
| 157 |
+
const dir = (sign * rotAxisSign) as 1 | -1;
|
| 158 |
+
return moveFromAxisSlice(rotAxis, slice, dir);
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
function projectDirToScreen(
|
| 162 |
+
camera: THREE.Camera,
|
| 163 |
+
worldPoint: THREE.Vector3,
|
| 164 |
+
dir: THREE.Vector3
|
| 165 |
+
): THREE.Vector2 {
|
| 166 |
+
const a = worldPoint.clone().project(camera);
|
| 167 |
+
const b = worldPoint.clone().add(dir).project(camera);
|
| 168 |
+
return new THREE.Vector2(b.x - a.x, b.y - a.y);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
function dominantAxis(v: THREE.Vector3): Axis | null {
|
| 172 |
+
const ax = Math.abs(v.x), ay = Math.abs(v.y), az = Math.abs(v.z);
|
| 173 |
+
if (ax > ay && ax > az) return 'x';
|
| 174 |
+
if (ay > ax && ay > az) return 'y';
|
| 175 |
+
if (az > ax && az > ay) return 'z';
|
| 176 |
+
return null;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// Map (axis, slice, dir) → move name. dir convention matches MoveAnimator (CW from +axis).
|
| 180 |
+
function moveFromAxisSlice(axis: Axis, slice: number, dir: 1 | -1): string | null {
|
| 181 |
+
// Lookup: for each (axis, slice) what is the canonical move and which dir is its base?
|
| 182 |
+
// Use BASE_MOVES table from animator.ts — replicated here to keep this module decoupled.
|
| 183 |
+
// axis y, slice +1 → U (base dir +1); slice -1 → D (base dir -1); slice 0 → E (base dir -1).
|
| 184 |
+
// axis x, slice +1 → R (+1); -1 → L (-1); 0 → M (-1).
|
| 185 |
+
// axis z, slice +1 → F (+1); -1 → B (-1); 0 → S (+1).
|
| 186 |
+
const TABLE: Record<Axis, Record<number, { name: string; baseDir: 1 | -1 }>> = {
|
| 187 |
+
y: { 1: { name: 'U', baseDir: 1 }, [-1]: { name: 'D', baseDir: -1 }, 0: { name: 'E', baseDir: -1 } },
|
| 188 |
+
x: { 1: { name: 'R', baseDir: 1 }, [-1]: { name: 'L', baseDir: -1 }, 0: { name: 'M', baseDir: -1 } },
|
| 189 |
+
z: { 1: { name: 'F', baseDir: 1 }, [-1]: { name: 'B', baseDir: -1 }, 0: { name: 'S', baseDir: 1 } }
|
| 190 |
+
};
|
| 191 |
+
const entry = TABLE[axis][slice];
|
| 192 |
+
if (!entry) return null;
|
| 193 |
+
// If dir matches baseDir, plain move; otherwise prime.
|
| 194 |
+
return dir === entry.baseDir ? entry.name : entry.name + "'";
|
| 195 |
+
}
|
frontend/src/ui/controls/keyboard.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { MoveAnimator } from '../../scene/cube/animator';
|
| 2 |
+
import { generateScramble } from '../../scene/cube/scramble';
|
| 3 |
+
|
| 4 |
+
export interface KeyboardOptions {
|
| 5 |
+
onScramble?: () => void;
|
| 6 |
+
onReset?: () => void;
|
| 7 |
+
// Triggered for any move enqueued through the keyboard.
|
| 8 |
+
onMove?: (move: string) => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const NOTATION_KEYS = new Set([
|
| 12 |
+
'U', 'D', 'L', 'R', 'F', 'B', 'M', 'E', 'S', 'X', 'Y', 'Z'
|
| 13 |
+
]);
|
| 14 |
+
|
| 15 |
+
export function attachKeyboard(animator: MoveAnimator, opts: KeyboardOptions = {}): () => void {
|
| 16 |
+
function onKey(ev: KeyboardEvent): void {
|
| 17 |
+
if (ev.repeat) return;
|
| 18 |
+
if (ev.metaKey || ev.ctrlKey || ev.altKey) return;
|
| 19 |
+
const key = ev.key;
|
| 20 |
+
if (key === ' ') {
|
| 21 |
+
ev.preventDefault();
|
| 22 |
+
const seq = generateScramble(20);
|
| 23 |
+
for (const m of seq) animator.enqueue(m);
|
| 24 |
+
opts.onScramble?.();
|
| 25 |
+
return;
|
| 26 |
+
}
|
| 27 |
+
if (key === 'Enter') {
|
| 28 |
+
opts.onReset?.();
|
| 29 |
+
return;
|
| 30 |
+
}
|
| 31 |
+
const upper = key.toUpperCase();
|
| 32 |
+
if (!NOTATION_KEYS.has(upper)) return;
|
| 33 |
+
// X/Y/Z are whole-cube rotations expressed lowercase in our move grammar.
|
| 34 |
+
const base = (upper === 'X' || upper === 'Y' || upper === 'Z') ? upper.toLowerCase() : upper;
|
| 35 |
+
const move = ev.shiftKey ? base + "'" : base;
|
| 36 |
+
if (animator.enqueue(move)) opts.onMove?.(move);
|
| 37 |
+
}
|
| 38 |
+
window.addEventListener('keydown', onKey);
|
| 39 |
+
return () => window.removeEventListener('keydown', onKey);
|
| 40 |
+
}
|
frontend/src/ui/debugger.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { State, FaceKey } from '../core/state';
|
| 2 |
+
import { isSolved } from '../core/state';
|
| 3 |
+
import { FACE_COLORS } from '../scene/cube/cube';
|
| 4 |
+
|
| 5 |
+
const FACE_ORDER: FaceKey[] = ['U', 'L', 'F', 'R', 'B', 'D'];
|
| 6 |
+
const HISTORY_LIMIT = 20;
|
| 7 |
+
|
| 8 |
+
export class DebuggerPanel {
|
| 9 |
+
private el: HTMLDivElement;
|
| 10 |
+
private historyEl!: HTMLDivElement;
|
| 11 |
+
private statusEl!: HTMLSpanElement;
|
| 12 |
+
private faceEls = new Map<FaceKey, HTMLDivElement[]>(); // 9 stickers per face
|
| 13 |
+
private history: string[] = [];
|
| 14 |
+
|
| 15 |
+
constructor(parent: HTMLElement) {
|
| 16 |
+
this.el = document.createElement('div');
|
| 17 |
+
this.el.id = 'debugger';
|
| 18 |
+
parent.appendChild(this.el);
|
| 19 |
+
|
| 20 |
+
const title = document.createElement('h3');
|
| 21 |
+
title.textContent = "Rubik's Debugger";
|
| 22 |
+
this.el.appendChild(title);
|
| 23 |
+
|
| 24 |
+
const status = document.createElement('div');
|
| 25 |
+
status.className = 'row';
|
| 26 |
+
status.innerHTML = `<span>Solved:</span><span id="dbg-solved" class="solved-no">no</span>`;
|
| 27 |
+
this.el.appendChild(status);
|
| 28 |
+
this.statusEl = status.querySelector('#dbg-solved') as HTMLSpanElement;
|
| 29 |
+
|
| 30 |
+
const facesWrap = document.createElement('div');
|
| 31 |
+
facesWrap.className = 'faces';
|
| 32 |
+
for (const face of FACE_ORDER) {
|
| 33 |
+
const fEl = document.createElement('div');
|
| 34 |
+
fEl.className = 'face';
|
| 35 |
+
const label = document.createElement('div');
|
| 36 |
+
label.className = 'label';
|
| 37 |
+
label.textContent = face;
|
| 38 |
+
fEl.appendChild(label);
|
| 39 |
+
const stickers: HTMLDivElement[] = [];
|
| 40 |
+
for (let i = 0; i < 9; i++) {
|
| 41 |
+
const s = document.createElement('div');
|
| 42 |
+
s.className = 'sticker';
|
| 43 |
+
fEl.appendChild(s);
|
| 44 |
+
stickers.push(s);
|
| 45 |
+
}
|
| 46 |
+
this.faceEls.set(face, stickers);
|
| 47 |
+
facesWrap.appendChild(fEl);
|
| 48 |
+
}
|
| 49 |
+
this.el.appendChild(facesWrap);
|
| 50 |
+
|
| 51 |
+
const histTitle = document.createElement('div');
|
| 52 |
+
histTitle.style.marginTop = '6px';
|
| 53 |
+
histTitle.textContent = 'History:';
|
| 54 |
+
this.el.appendChild(histTitle);
|
| 55 |
+
|
| 56 |
+
this.historyEl = document.createElement('div');
|
| 57 |
+
this.historyEl.className = 'history';
|
| 58 |
+
this.el.appendChild(this.historyEl);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
pushMove(move: string): void {
|
| 62 |
+
this.history.push(move);
|
| 63 |
+
if (this.history.length > HISTORY_LIMIT) this.history.splice(0, this.history.length - HISTORY_LIMIT);
|
| 64 |
+
this.historyEl.textContent = this.history.join(' ');
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
reset(): void {
|
| 68 |
+
this.history = [];
|
| 69 |
+
this.historyEl.textContent = '';
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
render(state: State): void {
|
| 73 |
+
for (const face of FACE_ORDER) {
|
| 74 |
+
const stickers = this.faceEls.get(face)!;
|
| 75 |
+
for (let i = 0; i < 9; i++) {
|
| 76 |
+
const color = FACE_COLORS[state[face][i]];
|
| 77 |
+
stickers[i].style.background = '#' + color.toString(16).padStart(6, '0');
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
const solved = isSolved(state);
|
| 81 |
+
this.statusEl.textContent = solved ? 'yes' : 'no';
|
| 82 |
+
this.statusEl.className = solved ? 'solved-yes' : 'solved-no';
|
| 83 |
+
}
|
| 84 |
+
}
|
frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"module": "ESNext",
|
| 5 |
+
"moduleResolution": "bundler",
|
| 6 |
+
"strict": true,
|
| 7 |
+
"noUnusedLocals": true,
|
| 8 |
+
"noUnusedParameters": true,
|
| 9 |
+
"noImplicitReturns": true,
|
| 10 |
+
"esModuleInterop": true,
|
| 11 |
+
"skipLibCheck": true,
|
| 12 |
+
"forceConsistentCasingInFileNames": true,
|
| 13 |
+
"resolveJsonModule": true,
|
| 14 |
+
"isolatedModules": true,
|
| 15 |
+
"lib": ["ES2020", "DOM", "DOM.Iterable"]
|
| 16 |
+
},
|
| 17 |
+
"include": ["src"]
|
| 18 |
+
}
|
frontend/vite.config.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite';
|
| 2 |
+
|
| 3 |
+
export default defineConfig({
|
| 4 |
+
base: './',
|
| 5 |
+
build: {
|
| 6 |
+
outDir: 'dist',
|
| 7 |
+
emptyOutDir: true,
|
| 8 |
+
target: 'es2020'
|
| 9 |
+
}
|
| 10 |
+
});
|
requirements.txt
CHANGED
|
@@ -1 +1,3 @@
|
|
| 1 |
-
gradio
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
fastapi
|
| 3 |
+
uvicorn
|