Matplotlib Cheatsheets 文档体系全解析:双页速查表、三阶讲义与仓库构建流程
2026/9/27 8:21:17
Raylib下载详情请进专栏上一篇文章
// ============================================================================ // 三阶魔方 · C++ + raylib (修复版) // · 鼠标拖背景 / 右键拖:自由旋转视角预览(轨道相机 + 滚轮缩放) // · 鼠标拖动魔方上的色块:按拖拽方向转动对应的一层 // · 键盘标准记号:R L U D F B(+Shift 逆时针)、M E S 中层、X Y Z 整体转 // 空格打乱、Backspace 撤销、C 复位、A 自动旋转 // // 本版修复: // 1) X/Y/Z 整体转原先只转 layer=0(等价于 M'/E'/S'),会把中心块转到别的面。 // 现改为三层同步转动(一次动画完成,不再是逐层播放)。 // 2) 空格打乱原先只往队列里塞 move,静止状态下队列无人消费 —— 打乱完全不生效。 // 现改为入队后立即开播。 // 3) 撤销动作本身会被记录进撤销栈(排队时无条件 push),导致来回撤销死循环。 // 现给每个 move 带上 record 标志,撤销不入栈、不计步。 // 4) isSolved() 原判据为「所有朝向矩阵 == 单位阵」,整体转或中层转之后明明 // 六面纯色却显示 scrambled。现改为「每个面是否单色」。 // 5) 拖拽判定方向时用贴纸中心的真实速度 ω×r,而不是只用面法线近似。 // 6) 打乱改用外层转动(WCA 习惯),且打乱不计入步数、不可撤销。 // // 编译(Linux): g++ rubiks_cube.cpp -o cube -std=c++17 -lraylib -lm -lpthread -ldl // 编译(macOS): g++ rubiks_cube.cpp -o cube -std=c++17 -lraylib -framework OpenGL -framework Cocoa -framework IOKit -framework CoreVideo // 编译(MinGW): g++ rubiks_cube.cpp -o cube.exe -std=c++17 -lraylib -lopengl32 -lgdi32 -lwinmm // ============================================================================ #include "raylib.h" #include "raymath.h" #include <cmath> #include <cstdio> #include <cstdlib> #include <vector> // ============================================================================ // 第一部分:与渲染无关的核心数据(小向量 / 魔方状态 / 转动逻辑) // ============================================================================ struct V3 { float x, y, z; }; static inline V3 mk(float x, float y, float z) { return V3{ x, y, z }; } static inline V3 operator+(V3 a, V3 b) { return V3{ a.x + b.x, a.y + b.y, a.z + b.z }; } static inline V3 operator-(V3 a, V3 b) { return V3{ a.x - b.x, a.y - b.y, a.z - b.z }; } static inline V3 operator*(V3 a, float s) { return V3{ a.x * s, a.y * s, a.z * s }; } static inline float dot(V3 a, V3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; } static inline V3 cross(V3 a, V3 b) { return V3{ a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x }; } static inline float len(V3 a) { return std::sqrt(dot(a, a)); } static inline V3 normalize(V3 a) { float l = len(a); return (l > 1e-6f) ? a * (1.0f / l) : mk(0, 0, 0); } // 三个坐标轴单位向量:0=X(R/L), 1=Y(U/D), 2=Z(F/B) static inline V3 axisVec(int a) { return mk(a == 0 ? 1.0f : 0.0f, a == 1 ? 1.0f : 0.0f, a == 2 ? 1.0f : 0.0f); } // 绕 axis 轴的右手旋转,ang>0 为 +90° 方向(dir=+1 时与 rotVecI 完全一致) static inline V3 rotAxisF(V3 v, int axis, float ang) { float c = std::cos(ang), s = std::sin(ang); if (axis == 0) return mk(v.x, v.y * c - v.z * s, v.y * s + v.z * c); if (axis == 1) return mk(v.x * c + v.z * s, v.y, -v.x * s + v.z * c); return mk(v.x * c - v.y * s, v.x * s + v.y * c, v.z); } // 整数版 90° 旋转(用于位置与朝向矩阵,避免浮点误差累积) static void rotVecI(int v[3], int axis, int dir) { int x = v[0], y = v[1], z = v[2]; if (axis == 0) { // 绕 X if (dir > 0) { v[1] = -z; v[2] = y; } else { v[1] = z; v[2] = -y; } } else if (axis == 1) { // 绕 Y if (dir > 0) { v[0] = z; v[2] = -x; } else { v[0] = -z; v[2] = x; } } else { // 绕 Z if (dir > 0) { v[0] = -y; v[1] = x; } else { v[0] = y; v[1] = -x; } } } // 面索引:0=+X(R) 1=-X(L) 2=+Y(U) 3=-Y(D) 4=+Z(F) 5=-Z(B) static inline int faceAxis(int f) { return f / 2; } static inline int faceSign(int f) { return (f % 2 == 0) ? 1 : -1; } struct Cubie { int pos[3]; // 位置坐标,取值 -1 / 0 / 1 int R[3][3]; // 朝向矩阵:世界向量 = R · 本地向量 int sticker[6]; // 本地方向上的贴纸颜色索引(0..5),-1 表示没有贴纸 }; // axis:转轴;dir:+1 / -1;mask:bit(layer+1) 置位表示该层一起转(整体转 = 0b111) struct Move { int axis, dir, mask; bool record; }; struct Cube { Cubie c[27]; void reset() { int i = 0; for (int x = -1; x <= 1; x++) for (int y = -1; y <= 1; y++) for (int z = -1; z <= 1; z++) { Cubie &b = c[i++]; b.pos[0] = x; b.pos[1] = y; b.pos[2] = z; for (int a = 0; a < 3; a++) for (int b2 = 0; b2 < 3; b2++) b.R[a][b2] = (a == b2) ? 1 : 0; // 初始朝向为单位阵,本地方向朝外的一面才有贴纸 for (int f = 0; f < 6; f++) { int a = faceAxis(f), s = faceSign(f); b.sticker[f] = (b.pos[a] * s == 1) ? f : -1; } } } // 转动一层:axis 轴、layer 层(-1/0/1)、dir=+1 右手 +90° void applyMove(int axis, int layer, int dir) { for (int i = 0; i < 27; i++) { Cubie &b = c[i]; if (b.pos[axis] != layer) continue; rotVecI(b.pos, axis, dir); // 位置旋转 for (int j = 0; j < 3; j++) { // 朝向矩阵左乘旋转矩阵 int col[3] = { b.R[0][j], b.R[1][j], b.R[2][j] }; rotVecI(col, axis, dir); b.R[0][j] = col[0]; b.R[1][j] = col[1]; b.R[2][j] = col[2]; } } } // 复原判定:每个世界面上的贴纸同色。 // 这样整体转(X/Y/Z)或中层转之后,只要六面纯色就仍算复原, // 也不会被「中心块自转」这种人眼看不出来的状态误判。 bool isSolved() const { int faceColor[6]; for (int g = 0; g < 6; g++) faceColor[g] = -1; for (int i = 0; i < 27; i++) { const Cubie &b = c[i]; for (int f = 0; f < 6; f++) { if (b.sticker[f] < 0) continue; int nl[3] = { 0, 0, 0 }; nl[faceAxis(f)] = faceSign(f); int nw[3]; // 贴纸在世界中的朝向 for (int a = 0; a < 3; a++) nw[a] = b.R[a][0] * nl[0] + b.R[a][1] * nl[1] + b.R[a][2] * nl[2]; int g = -1; // 该朝向对应哪个外面 for (int k = 0; k < 6; k++) { int a = faceAxis(k); if (nw[a] == faceSign(k) && nw[(a + 1) % 3] == 0 && nw[(a + 2) % 3] == 0) { g = k; break; } } if (g < 0) return false; if (faceColor[g] < 0) faceColor[g] = b.sticker[f]; else if (faceColor[g] != b.sticker[f]) return false; } } return true; } }; // ============================================================================ // 第二部分:渲染 + 交互 // ============================================================================ static const float HALF = 0.5f; // 小方块半边长 static const float SPACING = 1.04f; // 小方块中心间距(留出缝隙) static const float STICKER_K = 0.84f; // 贴纸相对小方块的比例 static const float LIFT = 0.006f; // 贴纸外凸,避免与黑底 z-fighting // 六个面的标准配色:R 红、L 橙、U 白、D 黄、F 绿、B 蓝 static const Color FACE_COLOR[6] = { { 200, 32, 58, 255 }, // +X R 红 { 255, 132, 0, 255 }, // -X L 橙 { 245, 245, 245, 255 }, // +Y U 白 { 255, 214, 0, 255 }, // -Y D 黄 { 0, 158, 96, 255 }, // +Z F 绿 { 0, 92, 190, 255 }, // -Z B 蓝 }; static const Color BODY_COLOR = { 22, 22, 26, 255 }; // 生成本地坐标下某个面的四个顶点(从外部看为逆时针,法线朝外) static void faceQuad(int f, float half, float lift, V3 out[4]) { int a = faceAxis(f), s = faceSign(f); V3 n = axisVec(a) * (float)s; V3 u = axisVec((a + 1) % 3) * half; V3 v = axisVec((a + 2) % 3) * half; V3 c = n * (HALF + lift); if (s > 0) { out[0] = c - u - v; out[1] = c + u - v; out[2] = c + u + v; out[3] = c - u + v; } else { out[0] = c - u - v; out[1] = c - u + v; out[2] = c + u + v; out[3] = c + u - v; } } static inline Vector3 toRay(V3 a) { return Vector3{ a.x, a.y, a.z }; } static void drawQuad(V3 p0, V3 p1, V3 p2, V3 p3, Color col) { DrawTriangle3D(toRay(p0), toRay(p1), toRay(p2), col); DrawTriangle3D(toRay(p0), toRay(p2), toRay(p3), col); } // 本地坐标 → 世界坐标:先乘朝向矩阵,再平移到位,最后叠加转动动画 static V3 toWorld(const Cubie &b, V3 p, int animAxis, int animMask, float ang) { V3 q = mk(b.R[0][0] * p.x + b.R[0][1] * p.y + b.R[0][2] * p.z, b.R[1][0] * p.x + b.R[1][1] * p.y + b.R[1][2] * p.z, b.R[2][0] * p.x + b.R[2][1] * p.y + b.R[2][2] * p.z); q = q + mk(b.pos[0] * SPACING, b.pos[1] * SPACING, b.pos[2] * SPACING); if (animAxis >= 0 && (animMask & (1 << (b.pos[animAxis] + 1)))) q = rotAxisF(q, animAxis, ang); return q; } static void drawCube(const Cube &cube, int animAxis, int animMask, float ang) { for (int i = 0; i < 27; i++) { const Cubie &b = cube.c[i]; for (int f = 0; f < 6; f++) { V3 q[4]; faceQuad(f, HALF, 0.0f, q); V3 w[4]; for (int k = 0; k < 4; k++) w[k] = toWorld(b, q[k], animAxis, animMask, ang); drawQuad(w[0], w[1], w[2], w[3], BODY_COLOR); // 黑色底块 if (b.sticker[f] >= 0) { // 彩色贴纸 faceQuad(f, HALF * STICKER_K, LIFT, q); for (int k = 0; k < 4; k++) w[k] = toWorld(b, q[k], animAxis, animMask, ang); drawQuad(w[0], w[1], w[2], w[3], FACE_COLOR[b.sticker[f]]); } } } } // 鼠标拾取结果:命中哪个方块的哪个面 struct Pick { int idx = -1; int face = -1; V3 center = { 0, 0, 0 }; V3 normal = { 0, 1, 0 }; Vector2 screen = { 0, 0 }; float radius = 0.0f; }; static bool pickFace(const Cube &cube, const Camera3D &cam, Vector2 mouse, Pick *out) { bool found = false; float bestScore = 1e9f; for (int i = 0; i < 27; i++) { const Cubie &b = cube.c[i]; for (int f = 0; f < 6; f++) { if (b.sticker[f] < 0) continue; // 只看有贴纸的外表面 V3 nl = axisVec(faceAxis(f)) * (float)faceSign(f); V3 nw = mk(b.R[0][0] * nl.x + b.R[0][1] * nl.y + b.R[0][2] * nl.z, b.R[1][0] * nl.x + b.R[1][1] * nl.y + b.R[1][2] * nl.z, b.R[2][0] * nl.x + b.R[2][1] * nl.y + b.R[2][2] * nl.z); V3 center = mk(b.pos[0] * SPACING, b.pos[1] * SPACING, b.pos[2] * SPACING) + nw * HALF; if (dot(nw, mk(cam.position.x, cam.position.y, cam.position.z) - center) < 0.05f) continue; // 背面 Vector2 sc = GetWorldToScreen(toRay(center), cam); // 用一条切边估算该面在屏幕上的半径 V3 t = cross(nw, mk(0, 1, 0)); if (len(t) < 0.1f) t = cross(nw, mk(1, 0, 0)); t = normalize(t); Vector2 sc2 = GetWorldToScreen(toRay(center + t * HALF), cam); float r = std::sqrt((sc2.x - sc.x) * (sc2.x - sc.x) + (sc2.y - sc.y) * (sc2.y - sc.y)); float d = std::sqrt((mouse.x - sc.x) * (mouse.x - sc.x) + (mouse.y - sc.y) * (mouse.y - sc.y)); float score = d / (r * 1.35f + 1.0f); if (score < bestScore) { bestScore = score; found = true; out->idx = i; out->face = f; out->center = center; out->normal = nw; out->screen = sc; out->radius = r; } } } return found && bestScore < 1.0f; } static inline Vector2 v2sub(Vector2 a, Vector2 b) { return Vector2{ a.x - b.x, a.y - b.y }; } static inline float v2len(Vector2 a) { return std::sqrt(a.x * a.x + a.y * a.y); } static inline Vector2 v2norm(Vector2 a) { float l = v2len(a); return (l > 1e-6f) ? Vector2{ a.x / l, a.y / l } : Vector2{ 0, 0 }; } static inline float v2dot(Vector2 a, Vector2 b) { return a.x * b.x + a.y * b.y; } static float easeOutCubic(float t) { float u = 1.0f - t; return 1.0f - u * u * u; } int main(void) { SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_WINDOW_RESIZABLE); InitWindow(1180, 760, "Rubik's Cube 3x3 - C++ / raylib"); SetTargetFPS(60); Cube cube; cube.reset(); Camera3D cam = { 0 }; cam.up = Vector3{ 0, 1, 0 }; cam.fovy = 45.0f; cam.projection = CAMERA_PERSPECTIVE; cam.target = Vector3{ 0, 0, 0 }; float yaw = 0.72f, pitch = 0.45f, dist = 8.6f; bool autoRotate = false; // 转动动画 bool animating = false; int animAxis = -1, animDir = 1, animMask = 0; float animT = 0.0f; const float ANIM_TIME = 0.20f; // 单次转动时长(秒) std::vector<Move> queue, undoStack; int moveCount = 0; // 播完一步后从队列取下一步(保持 record 标志,撤销不会再被记进撤销栈) auto pumpQueue = [&]() { if (animating || queue.empty()) return; Move m = queue.front(); queue.erase(queue.begin()); animating = true; animT = 0.0f; animAxis = m.axis; animDir = m.dir; animMask = m.mask; if (m.record) { undoStack.push_back(m); moveCount++; } }; // record=false 用于“撤销”和“打乱”本身:不入撤销栈、不计步 auto startMove = [&](int axis, int dir, int mask, bool record) { Move m{ axis, dir, mask, record }; if (animating) { queue.push_back(m); return; } // 前一步还没播完,先排队 animating = true; animT = 0.0f; animAxis = axis; animDir = dir; animMask = mask; if (record) { undoStack.push_back(m); moveCount++; } }; // 鼠标拖拽状态:1=转视角,2=转魔方层 bool dragging = false; int dragMode = 0; Pick pick; bool hasPick = false; Vector2 dragStart = { 0, 0 }; bool dragFired = false; while (!WindowShouldClose()) { float dt = GetFrameTime(); // ---------- 动画推进 ---------- if (animating) { float dur = queue.empty() ? ANIM_TIME : ANIM_TIME * 0.45f; // 连续播放时提速 animT += dt / dur; if (animT >= 1.0f) { for (int l = -1; l <= 1; l++) // 同步应用参与转动的所有层 if (animMask & (1 << (l + 1))) cube.applyMove(animAxis, l, animDir); animating = false; animAxis = -1; animMask = 0; pumpQueue(); // 接着播放队列里的下一步 } } float ang = animating ? (animDir * (PI * 0.5f) * easeOutCubic(animT)) : 0.0f; // ---------- 相机 ---------- if (autoRotate && !dragging) yaw += dt * 0.25f; float cp = std::cos(pitch); cam.position = Vector3{ dist * cp * std::sin(yaw), dist * std::sin(pitch), dist * cp * std::cos(yaw) }; // ---------- 鼠标拖拽 ---------- Vector2 mouse = GetMousePosition(); if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT) || IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { dragging = true; dragFired = false; dragStart = mouse; bool right = IsMouseButtonPressed(MOUSE_BUTTON_RIGHT); hasPick = (!right && !animating) ? pickFace(cube, cam, mouse, &pick) : false; dragMode = hasPick ? 2 : 1; } if (dragging && dragMode == 1) { // 空白处拖动 = 转视角 Vector2 d = v2sub(mouse, dragStart); yaw -= d.x * 0.0065f; pitch += d.y * 0.0065f; dragStart = mouse; if (pitch > 1.45f) pitch = 1.45f; if (pitch < -1.45f) pitch = -1.45f; } if (dragging && dragMode == 2 && !dragFired) { // 在魔方上拖动 = 转层 Vector2 delta = v2sub(mouse, dragStart); if (v2len(delta) > 12.0f) { int nAxis = faceAxis(pick.face); int bestAxis = -1; float bestScore = 0.0f; // 对每个可能的转轴,算出该贴纸的真实线速度方向 ω×r,再比较屏幕方向 for (int a = 0; a < 3; a++) { if (a == nAxis) continue; V3 mv = cross(axisVec(a), pick.center); // dir=+1 时贴纸的移动方向 if (len(mv) < 1e-4f) continue; Vector2 s0 = GetWorldToScreen(toRay(pick.center), cam); Vector2 s1 = GetWorldToScreen(toRay(pick.center + mv * 0.5f), cam); float sc = v2dot(v2norm(delta), v2norm(v2sub(s1, s0))); if (std::fabs(sc) > std::fabs(bestScore)) { bestScore = sc; bestAxis = a; } } if (bestAxis >= 0) { int layer = cube.c[pick.idx].pos[bestAxis]; startMove(bestAxis, bestScore > 0 ? 1 : -1, 1 << (layer + 1), true); dragFired = true; } } } if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) || IsMouseButtonReleased(MOUSE_BUTTON_RIGHT)) { dragging = false; dragMode = 0; hasPick = false; } float wheel = GetMouseWheelMove(); // 滚轮缩放 if (wheel != 0.0f) { dist -= wheel * 0.9f; if (dist < 4.5f) dist = 4.5f; if (dist > 22.0f) dist = 22.0f; } // ---------- 键盘:标准魔方记号 ---------- // dir: 从该面外侧看顺时针 = 绕对应轴右手 -90°(右侧/上层/前面为正层) bool inv = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); int sgn = inv ? -1 : 1; auto faceTurn = [&](int key, int axis, int layer, int baseDir) { if (IsKeyPressed(key)) startMove(axis, baseDir * sgn, 1 << (layer + 1), true); }; auto wholeTurn = [&](int key, int axis, int baseDir) { // 整体转:三层一起动 if (IsKeyPressed(key)) startMove(axis, baseDir * sgn, 0b111, true); }; faceTurn(KEY_R, 0, 1, -1); // R 右面顺时针 faceTurn(KEY_L, 0, -1, 1); // L 左面顺时针 faceTurn(KEY_U, 1, 1, -1); // U 上面顺时针 faceTurn(KEY_D, 1, -1, 1); // D 下面顺时针 faceTurn(KEY_F, 2, 1, -1); // F 前面顺时针 faceTurn(KEY_B, 2, -1, 1); // B 后面顺时针 faceTurn(KEY_M, 0, 0, 1); // M 中层(跟随 L) faceTurn(KEY_E, 1, 0, 1); // E 中层(跟随 D) faceTurn(KEY_S, 2, 0, -1); // S 中层(跟随 F) wholeTurn(KEY_X, 0, -1); // x 整体绕 R 方向 wholeTurn(KEY_Y, 1, -1); // y 整体绕 U 方向 wholeTurn(KEY_Z, 2, -1); // z 整体绕 F 方向 if (IsKeyPressed(KEY_SPACE)) { // 打乱(外层转动,不计步、不可撤销) undoStack.clear(); queue.clear(); moveCount = 0; int lastAxis = -1; for (int i = 0; i < 25; i++) { int a; do { a = GetRandomValue(0, 2); } while (a == lastAxis); lastAxis = a; int l = GetRandomValue(0, 1) ? 1 : -1; queue.push_back(Move{ a, GetRandomValue(0, 1) ? 1 : -1, 1 << (l + 1), false }); } pumpQueue(); // 关键:静止状态下也要开播 } if (IsKeyPressed(KEY_BACKSPACE)) { // 撤销一步 if (!queue.empty()) queue.pop_back(); else if (!undoStack.empty()) { Move m = undoStack.back(); undoStack.pop_back(); startMove(m.axis, -m.dir, m.mask, false); if (--moveCount < 0) moveCount = 0; } } if (IsKeyPressed(KEY_C) || IsKeyPressed(KEY_ENTER)) { // 复位 cube.reset(); queue.clear(); undoStack.clear(); animating = false; animAxis = -1; animMask = 0; moveCount = 0; } if (IsKeyPressed(KEY_A)) autoRotate = !autoRotate; // ---------- 绘制 ---------- BeginDrawing(); ClearBackground(Color{ 18, 20, 28, 255 }); BeginMode3D(cam); DrawPlane(Vector3{ 0, -3.2f, 0 }, Vector2{ 60, 60 }, Color{ 26, 29, 40, 255 }); drawCube(cube, animAxis, animMask, ang); EndMode3D(); DrawText("Drag a sticker -> turn that layer Drag background / right-drag -> rotate view Wheel -> zoom", 16, 14, 18, Color{ 200, 210, 225, 255 }); DrawText("R L U D F B (+Shift = prime) M E S X Y Z Space: scramble Backspace: undo C: reset A: auto-rotate", 16, 38, 18, Color{ 150, 165, 190, 255 }); char info[128]; std::snprintf(info, sizeof(info), "Moves: %d | %s | Auto-rotate: %s", moveCount, cube.isSolved() ? "SOLVED" : "scrambled", autoRotate ? "ON" : "OFF"); DrawText(info, 16, GetScreenHeight() - 30, 20, cube.isSolved() ? Color{ 120, 230, 150, 255 } : Color{ 235, 200, 120, 255 }); if (hasPick && dragging && dragMode == 2) DrawText("dragging...", 16, GetScreenHeight() - 56, 16, Color{ 130, 150, 180, 255 }); EndDrawing(); } CloseWindow(); return 0; }