用pygame和opengl写的三阶魔方,功能与之前的一致

# ==============================================
# 导入必要的库
# ==============================================
import math          # 数学计算
import random        # 随机数生成
import sys           # 系统相关功能
from dataclasses import dataclass  # 数据类装饰器

import pygame        # 游戏开发库
from pygame.locals import DOUBLEBUF, OPENGL  # Pygame显示模式

import magiccube     # 魔方求解库
from magiccube import BasicSolver  # 基础求解器

from OpenGL.GL import *      # OpenGL基础函数
from OpenGL.GLU import gluPerspective, gluLookAt  # OpenGL实用函数


# ==============================
#  magiccube state: ULFRBD (len 54)
# ==============================
SOLVED_ULFRBD = "YYYYYYYYY" "RRRRRRRRR" "GGGGGGGGG" "OOOOOOOOO" "BBBBBBBBB" "WWWWWWWWW"
FACE_ORDER = "ULFRBD"

RGB = {
    "Y": (245, 230, 0),
    "W": (245, 245, 245),
    "R": (220, 30, 30),
    "O": (255, 140, 0),
    "G": (20, 170, 60),
    "B": (30, 90, 220),
}
PLASTIC = (25, 25, 30)

def ensure_state54(state) -> str:
    state = str(state).replace(" ", "").replace("\n", "").replace("\r", "")
    if len(state) != 54:
        raise ValueError(f"Cube state length={len(state)} expected 54.")
    return state


# ----------------- face coords mapping -----------------
def build_face_coords(size=3):
    r = list(range(size))
    rr = list(range(size - 1, -1, -1))
    return {
        "L": [[(0, y, z) for z in r] for y in rr],
        "R": [[(size - 1, y, z) for z in rr] for y in rr],
        "D": [[(x, 0, z) for x in r] for z in rr],
        "U": [[(x, size - 1, z) for x in r] for z in r],
        "B": [[(x, y, 0) for x in rr] for y in rr],
        "F": [[(x, y, size - 1) for x in r] for y in rr],
    }

FACE_COORDS = build_face_coords(3)


# ----------------- discrete 90° rotation -----------------
def rot_pos_90(pos, axis, sign):
    x, y, z = pos
    if axis == "x":
        return (x, -z, y) if sign == +1 else (x, z, -y)
    if axis == "y":
        return (z, y, -x) if sign == +1 else (-z, y, x)
    return (-y, x, z) if sign == +1 else (y, -x, z)

MAP_X_POS = {"U": "F", "F": "D", "D": "B", "B": "U", "L": "L", "R": "R"}
MAP_Y_POS = {"F": "R", "R": "B", "B": "L", "L": "F", "U": "U", "D": "D"}
MAP_Z_POS = {"U": "L", "L": "D", "D": "R", "R": "U", "F": "F", "B": "B"}

def invert_map(m):
    return {v: k for k, v in m.items()}

MAP_X_NEG = invert_map(MAP_X_POS)
MAP_Y_NEG = invert_map(MAP_Y_POS)
MAP_Z_NEG = invert_map(MAP_Z_POS)

def rot_face_label(face, axis, sign):
    if axis == "x":
        return (MAP_X_POS if sign == +1 else MAP_X_NEG)[face]
    if axis == "y":
        return (MAP_Y_POS if sign == +1 else MAP_Y_NEG)[face]
    return (MAP_Z_POS if sign == +1 else MAP_Z_NEG)[face]

FACE_AXIS_LAYER = {
    "U": ("y", +1),
    "D": ("y", -1),
    "R": ("x", +1),
    "L": ("x", -1),
    "F": ("z", +1),
    "B": ("z", -1),
}


# ==============================
#  Cube model (cubies + stickers)
# ==============================
@dataclass
class Cubie:
    pos: tuple
    stickers: dict

class CubeModel:
    def __init__(self, state_ulfrbd: str):
        self.cubies = {}
        self.from_state_ulfrbd(state_ulfrbd)

    def copy(self):
        c = CubeModel.__new__(CubeModel)
        c.cubies = {p: Cubie(p, dict(cb.stickers)) for p, cb in self.cubies.items()}
        return c

    def from_state_ulfrbd(self, state_ulfrbd: str):
        s = ensure_state54(state_ulfrbd)
        cubies = {}
        idx = 0
        for face in FACE_ORDER:
            grid = FACE_COORDS[face]
            for row in grid:
                for (x0, y0, z0) in row:
                    col = s[idx]; idx += 1
                    pos = (x0 - 1, y0 - 1, z0 - 1)
                    cubies.setdefault(pos, Cubie(pos, {})).stickers[face] = col
        self.cubies = cubies

    def to_state_ulfrbd(self) -> str:
        out = []
        for face in FACE_ORDER:
            grid = FACE_COORDS[face]
            for row in grid:
                for (x0, y0, z0) in row:
                    pos = (x0 - 1, y0 - 1, z0 - 1)
                    out.append(self.cubies[pos].stickers[face])
        return ensure_state54("".join(out))

    def apply_layer_quarter(self, axis, layer_val, sign):
        idx = "xyz".index(axis)
        affected = [cb for cb in self.cubies.values() if cb.pos[idx] == layer_val]
        moved = {}
        for cb in affected:
            new_pos = rot_pos_90(cb.pos, axis, sign)
            new_st = {rot_face_label(f, axis, sign): c for f, c in cb.stickers.items()}
            moved[new_pos] = Cubie(new_pos, new_st)
        for cb in affected:
            del self.cubies[cb.pos]
        for p, cb in moved.items():
            self.cubies[p] = cb


# ==============================
#  moves
# ==============================
def norm_token(t: str) -> str:
    t = t.strip()
    if t.endswith("i"):
        t = t[:-1] + "'"
    return t

def parse_token(t: str):
    t = norm_token(t)
    if not t:
        return "", False, 0
    base = t[0].upper()
    prime = ("'" in t)
    count = 2 if ("2" in t) else 1
    return base, prime, count

def random_scramble(n=25):
    faces = list("UDLRFB")
    out = []
    last = None
    for _ in range(n):
        f = random.choice([x for x in faces if x != last])
        last = f
        out.append(f + random.choice(["", "'", "2"]))
    return out


# ----------------- calibrate sign vs magiccube.rotate(face) -----------------
def calibrate_face_signs():
    base_state = SOLVED_ULFRBD
    base_model = CubeModel(base_state)
    face_sign = {}

    for face in "UDLRFB":
        axis, layer = FACE_AXIS_LAYER[face]
        ref = magiccube.Cube(3, base_state)
        ref.rotate(face)
        target = ensure_state54(ref.get())

        found = None
        for sign in (+1, -1):
            test = base_model.copy()
            test.apply_layer_quarter(axis, layer, sign)
            if test.to_state_ulfrbd() == target:
                found = sign
                break
        if found is None:
            raise RuntimeError(f"校准失败:{face} 无法匹配 magiccube.rotate({face})")
        face_sign[face] = found

    return face_sign


# ----------------- strip X/Y/Z to keep cube fixed (try 8 combos) -----------------
def apply_rot_to_face_map(face_map, rot_old2new):
    inv = invert_map(rot_old2new)
    return {f: face_map[inv[f]] for f in "UDLRFB"}

def convert_moves_fixed_orientation(raw_moves, face_sign, model: CubeModel):
    rot_choices = {
        "X": [MAP_X_POS, MAP_X_NEG],
        "Y": [MAP_Y_POS, MAP_Y_NEG],
        "Z": [MAP_Z_POS, MAP_Z_NEG],
    }

    def try_combo(rotX, rotY, rotZ):
        face_map = {f: f for f in "UDLRFB"}
        out = []
        for mv in raw_moves:
            s = norm_token(mv if isinstance(mv, str) else str(mv))
            if not s:
                continue
            base, prime, count = parse_token(s)

            if base in "XYZ":
                rot = {"X": rotX, "Y": rotY, "Z": rotZ}[base]
                if prime:
                    rot = invert_map(rot)
                for _ in range(count):
                    face_map = apply_rot_to_face_map(face_map, rot)
                continue

            if base in "UDLRFB":
                phys = face_map[base]
                for _ in range(count):
                    out.append(phys + ("'" if prime else ""))
        return out

    def apply_face_token(m: CubeModel, tok: str):
        b, p, cnt = parse_token(tok)
        if b not in "UDLRFB":
            return
        axis, layer = FACE_AXIS_LAYER[b]
        sg = face_sign[b] * (-1 if p else +1)
        for _ in range(cnt):
            m.apply_layer_quarter(axis, layer, +1 if sg > 0 else -1)

    best = None
    for rotX in rot_choices["X"]:
        for rotY in rot_choices["Y"]:
            for rotZ in rot_choices["Z"]:
                fixed = try_combo(rotX, rotY, rotZ)
                test = model.copy()
                for t in fixed:
                    apply_face_token(test, t)
                if test.to_state_ulfrbd() == SOLVED_ULFRBD:
                    return fixed
                best = fixed
    print("警告:无法找到能通过自检的 X/Y/Z 映射组合(可能出现宽转等)。")
    return best or []


# ==============================
#  animation
# ==============================
def clamp(x, a, b):
    return a if x < a else b if x > b else x

@dataclass
class TurnAnim:
    face: str
    prime: bool
    axis: str
    layer: int
    sign: int
    duration: float
    t: float = 0.0

    def angle_deg(self):
        u = clamp(self.t / self.duration, 0.0, 1.0)
        u = 0.5 - 0.5 * math.cos(math.pi * u)
        return 90.0 * u * self.sign

    def done(self):
        return self.t >= self.duration


# ==============================
#  OpenGL helpers
# ==============================
FACE_ID = {"U": 1, "D": 2, "L": 3, "R": 4, "F": 5, "B": 6}  # for picking

def setup_gl(w, h):
    glViewport(0, 0, w, h)

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45.0, w / float(h), 0.1, 200.0)

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()

    glEnable(GL_DEPTH_TEST)
    glDepthFunc(GL_LEQUAL)
    glClearDepth(1.0)

    # ✅ 稳:先禁用剔除,避免“顶点绕向不一致导致缺面”
    glDisable(GL_CULL_FACE)

    glShadeModel(GL_SMOOTH)
    glEnable(GL_NORMALIZE)

    glEnable(GL_LIGHTING)
    glEnable(GL_LIGHT0)
    glLightfv(GL_LIGHT0, GL_POSITION, (6.0, 8.0, 10.0, 1.0))
    glLightfv(GL_LIGHT0, GL_DIFFUSE,  (1.0, 1.0, 1.0, 1.0))
    glLightfv(GL_LIGHT0, GL_SPECULAR, (1.0, 1.0, 1.0, 1.0))
    glLightModelfv(GL_LIGHT_MODEL_AMBIENT, (0.20, 0.20, 0.20, 1.0))

    glDisable(GL_DITHER)

    try:
        glEnable(GL_MULTISAMPLE)
    except Exception:
        pass

def orbit_eye(yaw, pitch, radius):
    cy = math.cos(yaw)
    sy = math.sin(yaw)
    cp = math.cos(pitch)
    sp = math.sin(pitch)
    x = radius * cp * sy
    y = radius * sp
    z = radius * cp * cy
    return (x, y, z)

def face_axis_vec(axis: str):
    return (1.0, 0.0, 0.0) if axis == "x" else (0.0, 1.0, 0.0) if axis == "y" else (0.0, 0.0, 1.0)

def set_material_rgb255(rgb, ambient_scale=0.30):
    r, g, b = (rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0)
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE,  (r, g, b, 1.0))
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT,  (r*ambient_scale, g*ambient_scale, b*ambient_scale, 1.0))
    # ✅ 更亮的高光
    glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, (0.85, 0.85, 0.85, 1.0))
    glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 80.0)
    glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, (0.0, 0.0, 0.0, 1.0))

def draw_cubelet(stickers: dict,
                 size=0.92,
                 sticker_inset=0.18,
                 sticker_lift=0.03,
                 pick_mode=False):
    """
    Draw a single cubelet centered at origin.
    - Normal pass: plastic + stickers with lighting
    - Pick pass: stickers only, encoded face id colors (no lighting)
    """
    half = size * 0.5

    def quad(n, v0, v1, v2, v3):
        glNormal3f(*n)
        glBegin(GL_QUADS)
        glVertex3f(*v0); glVertex3f(*v1); glVertex3f(*v2); glVertex3f(*v3)
        glEnd()

    faces = {
        "U": ((0, +1, 0), (-half, +half, -half), (+half, +half, -half), (+half, +half, +half), (-half, +half, +half)),
        "D": ((0, -1, 0), (-half, -half, +half), (+half, -half, +half), (+half, -half, -half), (-half, -half, -half)),
        "L": ((-1, 0, 0), (-half, -half, +half), (-half, -half, -half), (-half, +half, -half), (-half, +half, +half)),
        "R": ((+1, 0, 0), (+half, -half, -half), (+half, -half, +half), (+half, +half, +half), (+half, +half, -half)),
        "F": ((0, 0, +1), (-half, -half, +half), (+half, -half, +half), (+half, +half, +half), (-half, +half, +half)),
        "B": ((0, 0, -1), (+half, -half, -half), (-half, -half, -half), (-half, +half, -half), (+half, +half, -half)),
    }

    # plastic
    if not pick_mode:
        set_material_rgb255(PLASTIC, ambient_scale=0.35)
        for _, (n, a, b, c, d) in faces.items():
            quad(n, a, b, c, d)

    # stickers
    glEnable(GL_POLYGON_OFFSET_FILL)
    glPolygonOffset(-1.0, -1.0)

    for f, col_char in stickers.items():
        n, a, b, c, d = faces[f]

        def inset_vertex(v):
            x, y, z = v
            nx, ny, nz = n
            sx = 1.0 - (sticker_inset if nx == 0 else 0.0)
            sy = 1.0 - (sticker_inset if ny == 0 else 0.0)
            sz = 1.0 - (sticker_inset if nz == 0 else 0.0)
            x, y, z = x * sx, y * sy, z * sz
            x += nx * sticker_lift
            y += ny * sticker_lift
            z += nz * sticker_lift
            return (x, y, z)

        aa, bb, cc, dd = map(inset_vertex, (a, b, c, d))

        if pick_mode:
            fid = FACE_ID.get(f, 0)
            glColor3ub(fid * 30, 0, 0)
            quad(n, aa, bb, cc, dd)
        else:
            set_material_rgb255(RGB.get(col_char, (200, 0, 200)), ambient_scale=0.25)
            quad(n, aa, bb, cc, dd)

    glDisable(GL_POLYGON_OFFSET_FILL)


def render_scene(model: CubeModel, yaw, pitch, cam_radius, anim: TurnAnim | None, pick_mode=False):
    """渲染整个魔方场景"""
    # 设置背景颜色(深灰色)
    glClearColor(22/255.0, 22/255.0, 28/255.0, 1.0)
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    # 强制恢复状态:避免拾取过程影响正常渲染
    glDisable(GL_BLEND)
    glDisable(GL_TEXTURE_2D)

    # 根据模式设置光照
    if pick_mode:
        glDisable(GL_LIGHTING)
        glDisable(GL_LIGHT0)
    else:
        glEnable(GL_LIGHTING)
        glEnable(GL_LIGHT0)

    # 设置摄像机视角
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    ex, ey, ez = orbit_eye(yaw, pitch, cam_radius)
    gluLookAt(ex, ey, ez, 0, 0, 0, 0, 1, 0)  # 摄像机看向原点

    # 整体缩放魔方
    glPushMatrix()
    glScalef(1.35, 1.35, 1.35)

    # 绘制每个立方块
    for cb in model.cubies.values():
        glPushMatrix()

        # 应用动画旋转(如果当前立方块在旋转层中)
        if anim and cb.pos["xyz".index(anim.axis)] == anim.layer:
            ax = face_axis_vec(anim.axis)
            glRotatef(anim.angle_deg(), ax[0], ax[1], ax[2])

        # 平移到立方块位置
        x, y, z = cb.pos
        glTranslatef(float(x), float(y), float(z))
        
        # 绘制立方块
        draw_cubelet(cb.stickers, pick_mode=pick_mode)
        glPopMatrix()

    glPopMatrix()
    glFlush()  # 强制执行OpenGL命令


def pick_face_under_mouse(model, yaw, pitch, cam_radius, anim, mouse_xy, w, h):
    """
    颜色拾取:用贴纸颜色编码 face id
    注意:拾取时关闭多重采样/抖动,避免颜色被“边缘混合”污染
    """
    msaa_was = glIsEnabled(GL_MULTISAMPLE)
    dither_was = glIsEnabled(GL_DITHER)

    if msaa_was:
        glDisable(GL_MULTISAMPLE)
    if dither_was:
        glDisable(GL_DITHER)

    glReadBuffer(GL_BACK)
    render_scene(model, yaw, pitch, cam_radius, anim, pick_mode=True)
    glFlush()

    mx, my = mouse_xy
    ry = h - my - 1
    px = glReadPixels(mx, ry, 1, 1, GL_RGB, GL_UNSIGNED_BYTE)

    # restore
    if msaa_was:
        glEnable(GL_MULTISAMPLE)
    if dither_was:
        glEnable(GL_DITHER)

    if px is None:
        return None

    # PyOpenGL 在不同平台可能返回 bytes 或 ndarray-like
    if isinstance(px, (bytes, bytearray)):
        r = px[0]
    else:
        r = int(px[0][0][0])

    if r == 0:
        return None

    fid = int(round(r / 30.0))
    for face, _id in FACE_ID.items():
        if _id == fid:
            return face
    return None


# ==============================
#  main
# ==============================
def main():
    pygame.init()
    w, h = 1180, 780

    pygame.display.gl_set_attribute(pygame.GL_DEPTH_SIZE, 24)
    pygame.display.set_mode((w, h), DOUBLEBUF | OPENGL)
    pygame.display.set_caption("3D 魔方(pygame+OpenGL)- 专业鼠标手势版")

    clock = pygame.time.Clock()
    setup_gl(w, h)

    # logic init
    tmp = CubeModel(SOLVED_ULFRBD)
    if tmp.to_state_ulfrbd() != SOLVED_ULFRBD:
        raise SystemExit("ULFRBD<->模型转换不一致(程序错误)")

    face_sign = calibrate_face_signs()
    model = CubeModel(SOLVED_ULFRBD)

    # camera
    yaw, pitch = -0.7, 0.55
    cam_radius = 10.0

    # anim queue
    queue = []
    anim = None
    paused = False
    speed = 0.22
    last_move = ""

    def enqueue(tok: str):
        nonlocal queue
        tok = norm_token(tok)
        base, prime, count = parse_token(tok)
        if base not in "UDLRFB":
            return
        for _ in range(count):
            queue.append(base + ("'" if prime else ""))

    def start_next():
        nonlocal anim, last_move
        if anim or not queue:
            return
        tok = queue.pop(0)
        base, prime, _ = parse_token(tok)
        axis, layer = FACE_AXIS_LAYER[base]
        sg = face_sign[base] * (-1 if prime else +1)
        anim = TurnAnim(base, prime, axis, layer, +1 if sg > 0 else -1, speed)
        last_move = tok

    def commit():
        nonlocal anim
        if not anim:
            return
        model.apply_layer_quarter(anim.axis, anim.layer, anim.sign)
        anim = None

    def do_reset():
        nonlocal model, queue, anim, last_move
        model = CubeModel(SOLVED_ULFRBD)
        queue.clear()
        anim = None
        last_move = ""

    def do_solve():
        nonlocal queue, anim, paused
        paused = False
        queue.clear()
        anim = None

        state = model.to_state_ulfrbd()
        try:
            mc = magiccube.Cube(3, state, hist=True)
        except TypeError:
            mc = magiccube.Cube(3, state)

        solver = BasicSolver(mc)
        solver.solve()

        try:
            hist = mc.history(to_str=True)
        except TypeError:
            hist = mc.history()

        raw_moves = hist.split() if isinstance(hist, str) else [str(x) for x in hist]
        fixed = convert_moves_fixed_orientation(raw_moves, face_sign, model)
        for t in fixed:
            enqueue(t)

    def apply_face_turn(face, direction, ctrl=False):
        if face not in "UDLRFB":
            return
        if ctrl:
            enqueue(face + "2")
        else:
            enqueue(face if direction > 0 else (face + "'"))

    # ==============================
    #  专业鼠标手势状态机
    # ==============================
    # 点击判定阈值:短按 + 小位移
    CLICK_MAX_MS = 260
    CLICK_MOVE_PX = 6

    # 双键 Orbit:一旦进入,直到两键都松开前,完全屏蔽“点击拧面”
    suppress_until_all_up = False
    gesture = "idle"   # idle / clickL / clickR / orbit

    left_down = False
    right_down = False

    down_pos = (0, 0)
    down_time = 0
    click_dragged = False
    last_mouse = (0, 0)

    # 视角灵敏度(你觉得反向就把符号改掉)
    ORBIT_SENS = 0.006

    running = True
    while running:
        dt = clock.tick(60) / 1000.0
        mouse = pygame.mouse.get_pos()

        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                running = False

            elif e.type == pygame.KEYDOWN:
                mods = pygame.key.get_mods()
                shift = bool(mods & pygame.KMOD_SHIFT)
                ctrl = bool(mods & pygame.KMOD_CTRL)

                if e.key == pygame.K_ESCAPE:
                    running = False
                elif e.key in (pygame.K_BACKSPACE, pygame.K_x):
                    do_reset()
                elif e.key == pygame.K_p:
                    paused = not paused
                elif e.key in (pygame.K_EQUALS, pygame.K_PLUS, pygame.K_KP_PLUS):
                    speed = max(0.05, speed - 0.03)
                elif e.key in (pygame.K_MINUS, pygame.K_KP_MINUS):
                    speed = min(1.0, speed + 0.03)
                elif e.key == pygame.K_s:
                    queue.clear()
                    anim = None
                    paused = False
                    for m in random_scramble(25):
                        enqueue(m)
                elif e.key == pygame.K_RETURN:
                    do_solve()
                else:
                    keymap = {
                        pygame.K_u: "U",
                        pygame.K_d: "D",
                        pygame.K_l: "L",
                        pygame.K_r: "R",
                        pygame.K_f: "F",
                        pygame.K_b: "B",
                    }
                    if e.key in keymap:
                        base = keymap[e.key]
                        tok = base + ("2" if ctrl else ("'" if shift else ""))
                        paused = False
                        enqueue(tok)

            # 滚轮拧面:悬停在面上
            elif e.type == pygame.MOUSEWHEEL:
                mods = pygame.key.get_mods()
                ctrl = bool(mods & pygame.KMOD_CTRL)

                # 如果正在双键 orbit(或刚进入),仍然允许滚轮:你想禁掉也可以
                face = pick_face_under_mouse(model, yaw, pitch, cam_radius, anim, mouse, w, h)
                if face:
                    step = 1 if e.y > 0 else -1
                    for _ in range(abs(int(e.y)) or 1):
                        apply_face_turn(face, step, ctrl=ctrl)

            elif e.type == pygame.MOUSEBUTTONDOWN:
                if e.button == 1:
                    left_down = True
                elif e.button == 3:
                    right_down = True
                else:
                    continue

                down_pos = mouse
                down_time = pygame.time.get_ticks()
                click_dragged = False
                last_mouse = mouse

                # 进入双键:立即切换 orbit,并屏蔽点击直到两键都松开
                if left_down and right_down:
                    gesture = "orbit"
                    suppress_until_all_up = True
                else:
                    gesture = "clickL" if left_down else ("clickR" if right_down else "idle")

            elif e.type == pygame.MOUSEMOTION:
                mx, my = mouse
                lx, ly = last_mouse
                dx, dy = mx - lx, my - ly

                # 位移判定
                if abs(mx - down_pos[0]) + abs(my - down_pos[1]) > CLICK_MOVE_PX:
                    click_dragged = True

                # 双键 orbit:只有两键仍按下才旋转
                if gesture == "orbit" and left_down and right_down:
                    # ✅ 你说“反向”:专业默认是“鼠标往右拖 => 视角往右转”
                    yaw -= dx * ORBIT_SENS
                    pitch += dy * ORBIT_SENS
                    pitch = clamp(pitch, -1.25, 1.25)

                last_mouse = mouse

            elif e.type == pygame.MOUSEBUTTONUP:
                # 先更新键状态
                if e.button == 1:
                    left_down = False
                elif e.button == 3:
                    right_down = False
                else:
                    continue

                # ✅ 专业规则:只要进入过双键 orbit,就一直屏蔽“点击拧面”
                if suppress_until_all_up:
                    if (not left_down) and (not right_down):
                        suppress_until_all_up = False
                        gesture = "idle"
                    # 直接跳过点击拧面
                    continue

                # 单键点击拧面:必须满足短按+小位移,并且释放的是对应按钮
                t_up = pygame.time.get_ticks()
                is_click = (not click_dragged) and ((t_up - down_time) <= CLICK_MAX_MS)

                mods = pygame.key.get_mods()
                ctrl = bool(mods & pygame.KMOD_CTRL)

                if is_click:
                    if e.button == 1:
                        face = pick_face_under_mouse(model, yaw, pitch, cam_radius, anim, mouse, w, h)
                        if face:
                            apply_face_turn(face, +1, ctrl=ctrl)
                    elif e.button == 3:
                        face = pick_face_under_mouse(model, yaw, pitch, cam_radius, anim, mouse, w, h)
                        if face:
                            apply_face_turn(face, -1, ctrl=ctrl)

                gesture = "idle"

        # animate
        if not paused:
            if anim is None and queue:
                start_next()
            if anim is not None:
                anim.duration = speed
                anim.t += dt
                if anim.done():
                    commit()

        # render visible pass
        render_scene(model, yaw, pitch, cam_radius, anim, pick_mode=False)
        pygame.display.flip()

    pygame.quit()
    sys.exit(0)

if __name__ == "__main__":
    main()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值