|
|
@@ -0,0 +1,608 @@
|
|
|
+import pygame
|
|
|
+import math
|
|
|
+import time
|
|
|
+import os
|
|
|
+try:
|
|
|
+ import pyperclip
|
|
|
+ clipboard_available = True
|
|
|
+except ImportError:
|
|
|
+ clipboard_available = False
|
|
|
+ print("警告: pyperclip 模块未安装,剪贴板功能不可用。请运行 'pip install pyperclip' 安装。")
|
|
|
+
|
|
|
+# 尝试导入 pika 库用于 RabbitMQ 通信
|
|
|
+try:
|
|
|
+ import pika
|
|
|
+ rabbitmq_available = True
|
|
|
+except ImportError:
|
|
|
+ rabbitmq_available = False
|
|
|
+ print("警告: pika 模块未安装,RabbitMQ 通信功能不可用。请运行 'pip install pika' 安装。")
|
|
|
+
|
|
|
+from pygame.locals import *
|
|
|
+from OpenGL.GL import *
|
|
|
+from OpenGL.GLU import *
|
|
|
+from OpenGL.GLUT import *
|
|
|
+
|
|
|
+EXCLUDED_DIR = ['.git', '.vscode', '.idea', 'node_modules', '__pycache__']
|
|
|
+
|
|
|
+# 初始化字体
|
|
|
+pygame.font.init()
|
|
|
+# 使用默认字体,增大字体大小
|
|
|
+font = pygame.font.Font(None, 24)
|
|
|
+
|
|
|
+# 初始化 GLUT
|
|
|
+glutInit()
|
|
|
+
|
|
|
+# RabbitMQ 配置
|
|
|
+RABBITMQ_HOST = '101.201.78.54'
|
|
|
+RABBITMQ_PORT = 5672
|
|
|
+RABBITMQ_QUEUE = 'file_matches'
|
|
|
+RABBITMQ_USERNAME = 'admin' # 默认用户名
|
|
|
+RABBITMQ_PASSWORD = 'zpq123456' # 默认密码
|
|
|
+
|
|
|
+# 发送消息到 RabbitMQ
|
|
|
+def send_to_rabbitmq(message):
|
|
|
+ """发送消息到 RabbitMQ"""
|
|
|
+ if not rabbitmq_available:
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ # 建立连接
|
|
|
+ credentials = pika.PlainCredentials(RABBITMQ_USERNAME, RABBITMQ_PASSWORD)
|
|
|
+ connection = pika.BlockingConnection(pika.ConnectionParameters(
|
|
|
+ host=RABBITMQ_HOST, port=RABBITMQ_PORT, credentials=credentials
|
|
|
+ ))
|
|
|
+ channel = connection.channel()
|
|
|
+
|
|
|
+ # 声明队列(使用持久化队列,避免弃用警告)
|
|
|
+ channel.queue_declare(queue=RABBITMQ_QUEUE, durable=True)
|
|
|
+
|
|
|
+ # 发送消息(不使用 BasicProperties,简化发送方式)
|
|
|
+ channel.basic_publish(
|
|
|
+ exchange='',
|
|
|
+ routing_key=RABBITMQ_QUEUE,
|
|
|
+ body=message.encode('utf-8')
|
|
|
+ )
|
|
|
+
|
|
|
+ # 关闭连接
|
|
|
+ connection.close()
|
|
|
+ print(f"消息发送成功: {message}")
|
|
|
+ except Exception as e:
|
|
|
+ print(f"发送消息到 RabbitMQ 失败: {e}")
|
|
|
+
|
|
|
+# 模拟模式开关(True: 使用模拟数据,False: 扫描真实目录)
|
|
|
+# USE_MOCK_DATA = True
|
|
|
+USE_MOCK_DATA = False
|
|
|
+
|
|
|
+# 初始化 Pygame
|
|
|
+pygame.init()
|
|
|
+
|
|
|
+# 获取屏幕尺寸
|
|
|
+info = pygame.display.Info()
|
|
|
+screen_width = info.current_w
|
|
|
+screen_height = info.current_h
|
|
|
+
|
|
|
+# 设置窗口大小(固定宽度为1000px,固定高度为1080px)
|
|
|
+width = 1000 # 固定宽度为1000像素
|
|
|
+height = 1800 # 固定高度为1080像素
|
|
|
+display = pygame.display.set_mode((width, height), DOUBLEBUF | OPENGL)
|
|
|
+pygame.display.set_caption('3D Yellow Ball')
|
|
|
+
|
|
|
+# 启用深度测试
|
|
|
+glEnable(GL_DEPTH_TEST)
|
|
|
+
|
|
|
+# 设置背景色
|
|
|
+glClearColor(0.1, 0.1, 0.1, 1.0)
|
|
|
+# 禁用混合,确保完全不透明
|
|
|
+glDisable(GL_BLEND)
|
|
|
+
|
|
|
+# 设置光照
|
|
|
+glEnable(GL_LIGHTING)
|
|
|
+glEnable(GL_LIGHT0)
|
|
|
+
|
|
|
+# 设置光源位置
|
|
|
+light_position = [2.0, 2.0, 2.0, 1.0] # 点光源
|
|
|
+glLightfv(GL_LIGHT0, GL_POSITION, light_position)
|
|
|
+
|
|
|
+# 设置光源颜色
|
|
|
+light_ambient = [0.3, 0.3, 0.3, 1.0]
|
|
|
+light_diffuse = [1.0, 1.0, 1.0, 1.0]
|
|
|
+light_specular = [1.0, 1.0, 1.0, 1.0]
|
|
|
+glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient)
|
|
|
+glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse)
|
|
|
+glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular)
|
|
|
+
|
|
|
+# 设置投影矩阵(透视投影)
|
|
|
+glMatrixMode(GL_PROJECTION)
|
|
|
+glLoadIdentity()
|
|
|
+gluPerspective(45, (width/height), 0.1, 50.0)
|
|
|
+
|
|
|
+# 设置模型视图矩阵
|
|
|
+glMatrixMode(GL_MODELVIEW)
|
|
|
+glLoadIdentity()
|
|
|
+# 移动相机(现在在主循环中每帧设置)
|
|
|
+# glTranslatef(0.0, 0.0, -5)
|
|
|
+
|
|
|
+def generate_mock_tree():
|
|
|
+ # 生成模拟目录树数据
|
|
|
+ nodes = [
|
|
|
+ ("root", True, -1, 0, "/mock/root"),
|
|
|
+ ("dir1", True, 0, 1, "/mock/root/dir1"),
|
|
|
+ ("dir2", True, 0, 1, "/mock/root/dir2"),
|
|
|
+ ("file1.txt", False, 0, 1, "/mock/root/file1.txt"),
|
|
|
+ ("file2.py", False, 0, 1, "/mock/root/file2.py"),
|
|
|
+ ("subdir1", True, 1, 2, "/mock/root/dir1/subdir1"),
|
|
|
+ ("file3.js", False, 1, 2, "/mock/root/dir1/file3.js"),
|
|
|
+ ("file4.md", False, 2, 2, "/mock/root/dir2/file4.md"),
|
|
|
+ ]
|
|
|
+
|
|
|
+ edges = [
|
|
|
+ (0, 1), (0, 2), (0, 3), (0, 4),
|
|
|
+ (1, 5), (1, 6),
|
|
|
+ (2, 7),
|
|
|
+ ]
|
|
|
+
|
|
|
+ # 构建子节点列表
|
|
|
+ children = [[] for _ in range(len(nodes))]
|
|
|
+ for parent_id, child_id in edges:
|
|
|
+ children[parent_id].append(child_id)
|
|
|
+
|
|
|
+ # 计算节点位置(使用原有算法)
|
|
|
+ positions = []
|
|
|
+ for i in range(len(nodes)):
|
|
|
+ positions.append([0.0, 0.0, 0.0])
|
|
|
+
|
|
|
+ # 计算最大深度
|
|
|
+ max_depth_found = 0
|
|
|
+ for node in nodes:
|
|
|
+ max_depth_found = max(max_depth_found, node[3])
|
|
|
+
|
|
|
+ # 递归计算位置函数
|
|
|
+ def calculate_positions(node_id, start_angle, end_angle, depth):
|
|
|
+ if depth > max_depth_found:
|
|
|
+ return
|
|
|
+
|
|
|
+ if node_id == 0: # 根节点
|
|
|
+ positions[node_id] = [0.0, max_depth_found * 0.8, 0.0]
|
|
|
+ else:
|
|
|
+ parent_id = nodes[node_id][2]
|
|
|
+ if parent_id >= 0:
|
|
|
+ child_index = children[parent_id].index(node_id)
|
|
|
+ num_siblings = len(children[parent_id])
|
|
|
+ angle = start_angle + (end_angle - start_angle) * (child_index / max(num_siblings, 1))
|
|
|
+ radius = 0.75 * (0.75 ** depth)
|
|
|
+ px, py, pz = positions[parent_id]
|
|
|
+ x = px + radius * math.cos(angle)
|
|
|
+ y = py - 0.65
|
|
|
+ z = pz + radius * math.sin(angle)
|
|
|
+ positions[node_id] = [x, y, z]
|
|
|
+
|
|
|
+ node_children = children[node_id]
|
|
|
+ if node_children:
|
|
|
+ # 计算子节点的角度范围
|
|
|
+ angle_range = math.pi * 2.0 # 360度范围
|
|
|
+ angle_start = -angle_range / 2
|
|
|
+ angle_step = angle_range / len(node_children)
|
|
|
+
|
|
|
+ for i, child_id in enumerate(node_children):
|
|
|
+ child_angle_start = angle_start + i * angle_step
|
|
|
+ child_angle_end = angle_start + (i + 1) * angle_step
|
|
|
+ calculate_positions(child_id, child_angle_start, child_angle_end, depth + 1)
|
|
|
+
|
|
|
+ calculate_positions(0, -math.pi, math.pi, 0)
|
|
|
+ return nodes, edges, positions, children
|
|
|
+
|
|
|
+def build_directory_tree(root_path, max_depth=3, max_children_per_node=30):
|
|
|
+ # 如果启用模拟模式,返回模拟数据
|
|
|
+ if USE_MOCK_DATA:
|
|
|
+ return generate_mock_tree()
|
|
|
+
|
|
|
+ # 递归扫描目录树
|
|
|
+ nodes = [] # 每个节点: (name, is_dir, parent_id, depth)
|
|
|
+ edges = [] # (parent_id, child_id)
|
|
|
+
|
|
|
+ # 递归扫描函数
|
|
|
+ def scan_directory(current_path, parent_id, depth):
|
|
|
+ if depth >= max_depth:
|
|
|
+ return
|
|
|
+
|
|
|
+ try:
|
|
|
+ entries = os.listdir(current_path)
|
|
|
+ except Exception:
|
|
|
+ return # 忽略所有访问错误
|
|
|
+
|
|
|
+ # 过滤掉一些不需要的目录
|
|
|
+ filtered_entries = []
|
|
|
+ for entry in entries:
|
|
|
+ # 过滤常见的版本控制和IDE目录
|
|
|
+ if entry in EXCLUDED_DIR:
|
|
|
+ continue
|
|
|
+ filtered_entries.append(entry)
|
|
|
+
|
|
|
+ # 限制每个节点的最大子项数量
|
|
|
+ filtered_entries = filtered_entries[:max_children_per_node]
|
|
|
+
|
|
|
+ # 当前目录的节点ID:根节点为0,其他目录为parent_id
|
|
|
+ current_node_id = 0 if parent_id == -1 else parent_id
|
|
|
+
|
|
|
+ for entry in filtered_entries:
|
|
|
+ entry_path = os.path.join(current_path, entry)
|
|
|
+ is_dir = os.path.isdir(entry_path)
|
|
|
+
|
|
|
+ # 添加子节点
|
|
|
+ child_id = len(nodes)
|
|
|
+ nodes.append((entry, is_dir, current_node_id, depth + 1, entry_path))
|
|
|
+ edges.append((current_node_id, child_id))
|
|
|
+
|
|
|
+ # 如果是目录,递归扫描
|
|
|
+ if is_dir:
|
|
|
+ scan_directory(entry_path, child_id, depth + 1)
|
|
|
+
|
|
|
+ # 添加根节点
|
|
|
+ root_name = os.path.basename(root_path.rstrip('\\'))
|
|
|
+ nodes.append((root_name, True, -1, 0, root_path))
|
|
|
+
|
|
|
+ # 从根节点开始递归扫描
|
|
|
+ scan_directory(root_path, -1, 0)
|
|
|
+
|
|
|
+ # 计算节点位置
|
|
|
+ positions = []
|
|
|
+ for i in range(len(nodes)):
|
|
|
+ positions.append([0.0, 0.0, 0.0]) # 初始位置
|
|
|
+
|
|
|
+ # 构建子节点列表
|
|
|
+ children = [[] for _ in range(len(nodes))]
|
|
|
+ for edge in edges:
|
|
|
+ parent_id, child_id = edge
|
|
|
+ children[parent_id].append(child_id)
|
|
|
+
|
|
|
+ # 计算最大深度
|
|
|
+ max_depth_found = 0
|
|
|
+ for node in nodes:
|
|
|
+ max_depth_found = max(max_depth_found, node[3])
|
|
|
+
|
|
|
+ # 递归计算位置
|
|
|
+ def calculate_positions(node_id, start_angle, end_angle, depth):
|
|
|
+ if depth > max_depth_found:
|
|
|
+ return
|
|
|
+
|
|
|
+ # 计算当前节点位置
|
|
|
+ if node_id == 0: # 根节点
|
|
|
+ # positions[node_id] = [0.0, max_depth_found * 0.4, 0.0] # 顶部,高度减半
|
|
|
+ positions[node_id] = [0.0, max_depth_found * 0.8, 0.0] # 更靠近顶部
|
|
|
+ else:
|
|
|
+ parent_id = nodes[node_id][2]
|
|
|
+ if parent_id >= 0:
|
|
|
+ # 子节点围绕父节点在圆上分布
|
|
|
+ child_index = children[parent_id].index(node_id)
|
|
|
+ # 用于表示某个节点的兄弟节点数量
|
|
|
+ num_siblings = len(children[parent_id])
|
|
|
+
|
|
|
+ # 计算角度(在父节点周围的圆上)
|
|
|
+ # 这行代码确保多个子节点在指定的角度范围内均匀分布,避免它们在3D空间中重叠或拥挤。例如,如果起始角度是0度,结束角度是360度,有4个子节点,那么它们的角度会分别是0度、90度、180度和270度,均匀分布在圆周上
|
|
|
+ angle = start_angle + (end_angle - start_angle) * (child_index / max(num_siblings, 1))
|
|
|
+
|
|
|
+ # 半径随深度减小
|
|
|
+ # radius = 0.75 * (0.75 ** depth) # 深度越大,半径越小,基础半径减半
|
|
|
+ if depth == 1:
|
|
|
+ radius = 1.5
|
|
|
+ else:
|
|
|
+ radius = 0.75 * (0.75 ** depth) # 深度越大,半径越小,基础半径减半
|
|
|
+
|
|
|
+ # 计算位置
|
|
|
+ px, py, pz = positions[parent_id]
|
|
|
+ x = px + radius * math.cos(angle)
|
|
|
+ y = py - 0.65 # 每层向下移动,垂直距离减半
|
|
|
+ z = pz + radius * math.sin(angle)
|
|
|
+
|
|
|
+ positions[node_id] = [x, y, z]
|
|
|
+
|
|
|
+ # 为子节点递归计算位置
|
|
|
+ node_children = children[node_id]
|
|
|
+ if node_children:
|
|
|
+ # 计算子节点的角度范围
|
|
|
+ angle_range = math.pi * 2.0 # 360度范围
|
|
|
+ angle_start = -angle_range / 2
|
|
|
+ angle_step = angle_range / len(node_children)
|
|
|
+
|
|
|
+ for i, child_id in enumerate(node_children):
|
|
|
+ child_angle_start = angle_start + i * angle_step
|
|
|
+ child_angle_end = angle_start + (i + 1) * angle_step
|
|
|
+ calculate_positions(child_id, child_angle_start, child_angle_end, depth + 1)
|
|
|
+
|
|
|
+ # 从根节点开始计算位置
|
|
|
+ calculate_positions(0, -math.pi, math.pi, 0)
|
|
|
+
|
|
|
+ return nodes, edges, positions, children
|
|
|
+
|
|
|
+# 构建目录树(使用当前目录)
|
|
|
+# root_path = "."
|
|
|
+root_path = "E:\\agricultural_research_platform"
|
|
|
+
|
|
|
+tree_nodes, tree_edges, node_positions, tree_children = build_directory_tree(root_path)
|
|
|
+
|
|
|
+def draw_text(x, y, z, text, color=(1.0, 1.0, 1.0)):
|
|
|
+ """在3D空间中绘制文本"""
|
|
|
+ glDisable(GL_LIGHTING)
|
|
|
+ glColor3f(*color) # 使用指定的颜色
|
|
|
+
|
|
|
+ # 保存当前矩阵
|
|
|
+ glPushMatrix()
|
|
|
+
|
|
|
+ # 移动到指定位置
|
|
|
+ glTranslatef(x, y, z)
|
|
|
+
|
|
|
+ # 旋转文本,使其面向相机
|
|
|
+ glRotatef(rotation_angle, 0.0, 1.0, 0.0)
|
|
|
+
|
|
|
+ # 缩小文本,使其适合3D空间
|
|
|
+ scale = 0.001
|
|
|
+ glScalef(scale, scale, scale)
|
|
|
+
|
|
|
+ # 计算文本宽度,以便居中显示
|
|
|
+ text_width = 0
|
|
|
+ for char in text:
|
|
|
+ text_width += glutBitmapWidth(GLUT_BITMAP_HELVETICA_12, ord(char))
|
|
|
+
|
|
|
+ # 设置文本位置,使其居中
|
|
|
+ glRasterPos2f(-text_width / 2, 0)
|
|
|
+
|
|
|
+ # 绘制文本
|
|
|
+ for char in text:
|
|
|
+ glutBitmapCharacter(GLUT_BITMAP_HELVETICA_12, ord(char))
|
|
|
+
|
|
|
+ # 恢复矩阵
|
|
|
+ glPopMatrix()
|
|
|
+ glEnable(GL_LIGHTING)
|
|
|
+
|
|
|
+# 初始化选中节点(根节点)
|
|
|
+selected_node_index = 0
|
|
|
+
|
|
|
+# 剪贴板相关变量
|
|
|
+clipboard_content = ""
|
|
|
+clipboard_check_time = 0
|
|
|
+blinking_nodes = set()
|
|
|
+file_content_cache = {}
|
|
|
+blink_start_time = pygame.time.get_ticks()
|
|
|
+
|
|
|
+# 旋转控制变量
|
|
|
+rotation_angle = 0.0
|
|
|
+
|
|
|
+# 相机控制变量
|
|
|
+camera_z = -5 # 相机的 Z 轴位置,初始值为 -5
|
|
|
+
|
|
|
+# 鼠标控制旋转变量
|
|
|
+mouse_dragging = False
|
|
|
+last_mouse_x, last_mouse_y = 0, 0
|
|
|
+rotation_x, rotation_y = 0.0, 0.0 # 分别控制上下和左右旋转
|
|
|
+
|
|
|
+# 主循环
|
|
|
+running = True
|
|
|
+while running:
|
|
|
+ # 获取当前时间(用于闪烁效果)
|
|
|
+ current_time = pygame.time.get_ticks()
|
|
|
+
|
|
|
+ # 更新旋转角度(降低旋转速度,现在大约每60秒旋转一圈)
|
|
|
+ rotation_angle = (current_time * 0.0005) % 360.0
|
|
|
+
|
|
|
+ # 检查剪贴板内容是否变化(如果剪贴板功能可用)
|
|
|
+ if clipboard_available:
|
|
|
+ if current_time - clipboard_check_time > 1000: # 每秒检查一次
|
|
|
+ clipboard_check_time = current_time
|
|
|
+ try:
|
|
|
+ new_clipboard_content = pyperclip.paste()
|
|
|
+ if new_clipboard_content != clipboard_content:
|
|
|
+ clipboard_content = new_clipboard_content
|
|
|
+ # 剪贴板内容变化,重新检查所有文件节点
|
|
|
+ blinking_nodes.clear()
|
|
|
+ if clipboard_content.strip(): # 剪贴板非空
|
|
|
+ for i, (name, is_dir, parent_id, depth, full_path) in enumerate(tree_nodes):
|
|
|
+ if not is_dir: # 文件节点
|
|
|
+ # 检查文件内容是否包含剪贴板文本
|
|
|
+ try:
|
|
|
+ # 从缓存读取或加载文件内容
|
|
|
+ if i not in file_content_cache:
|
|
|
+ with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
|
+ file_content_cache[i] = f.read()
|
|
|
+ file_content = file_content_cache[i]
|
|
|
+ if clipboard_content in file_content:
|
|
|
+ blinking_nodes.add(i)
|
|
|
+ # 发送文件名称到 RabbitMQ
|
|
|
+ send_to_rabbitmq(name)
|
|
|
+ except Exception as e:
|
|
|
+ pass # 忽略读取错误
|
|
|
+ except Exception as e:
|
|
|
+ pass # 忽略剪贴板访问错误
|
|
|
+
|
|
|
+ # 处理事件
|
|
|
+ for event in pygame.event.get():
|
|
|
+ if event.type == pygame.QUIT:
|
|
|
+ running = False
|
|
|
+ elif event.type == pygame.KEYDOWN:
|
|
|
+ # 获取当前节点信息
|
|
|
+ current_node = tree_nodes[selected_node_index]
|
|
|
+ current_parent_id = current_node[2]
|
|
|
+ current_depth = current_node[3]
|
|
|
+
|
|
|
+ if event.key == pygame.K_UP: # 上键:移动到父节点
|
|
|
+ if current_parent_id >= 0: # 有父节点
|
|
|
+ selected_node_index = current_parent_id
|
|
|
+ elif event.key == pygame.K_DOWN: # 下键:移动到第一个子节点
|
|
|
+ if tree_children[selected_node_index]: # 有子节点
|
|
|
+ selected_node_index = tree_children[selected_node_index][0]
|
|
|
+ elif event.key == pygame.K_LEFT: # 左键:移动到前一个兄弟节点
|
|
|
+ if current_parent_id >= 0: # 有父节点
|
|
|
+ siblings = tree_children[current_parent_id]
|
|
|
+ if len(siblings) > 1:
|
|
|
+ current_index_in_siblings = siblings.index(selected_node_index)
|
|
|
+ # 移动到前一个兄弟,如果当前是第一个则移动到最后一个
|
|
|
+ new_index = (current_index_in_siblings - 1) % len(siblings)
|
|
|
+ selected_node_index = siblings[new_index]
|
|
|
+ elif event.key == pygame.K_RIGHT: # 右键:移动到后一个兄弟节点
|
|
|
+ if current_parent_id >= 0: # 有父节点
|
|
|
+ siblings = tree_children[current_parent_id]
|
|
|
+ if len(siblings) > 1:
|
|
|
+ current_index_in_siblings = siblings.index(selected_node_index)
|
|
|
+ # 移动到后一个兄弟,如果当前是最后一个则移动到第一个
|
|
|
+ new_index = (current_index_in_siblings + 1) % len(siblings)
|
|
|
+ selected_node_index = siblings[new_index]
|
|
|
+ elif event.type == pygame.MOUSEBUTTONDOWN:
|
|
|
+ if event.button == 1: # 左键按下,开始拖拽旋转
|
|
|
+ mouse_dragging = True
|
|
|
+ last_mouse_x, last_mouse_y = pygame.mouse.get_pos()
|
|
|
+ elif event.button == 4: # 鼠标滚轮向上滚动,放大
|
|
|
+ camera_z += 0.5 # 相机向Z轴正方向移动,放大视图
|
|
|
+ elif event.button == 5: # 鼠标滚轮向下滚动,缩小
|
|
|
+ camera_z -= 0.5 # 相机向Z轴负方向移动,缩小视图
|
|
|
+
|
|
|
+ # 限制相机的Z轴范围,防止过度放大或缩小
|
|
|
+ camera_z = max(-10, min(-2, camera_z))
|
|
|
+ elif event.type == pygame.MOUSEBUTTONUP:
|
|
|
+ if event.button == 1: # 左键释放,停止拖拽
|
|
|
+ mouse_dragging = False
|
|
|
+ elif event.type == pygame.MOUSEMOTION:
|
|
|
+ if mouse_dragging:
|
|
|
+ # 获取鼠标移动距离
|
|
|
+ mouse_dx, mouse_dy = pygame.mouse.get_rel()
|
|
|
+
|
|
|
+ # 更新旋转角度(水平旋转绕Y轴,垂直旋转绕X轴)
|
|
|
+ rotation_y += mouse_dx * 0.5 # 水平拖拽影响Y轴旋转
|
|
|
+ rotation_x += mouse_dy * 0.5 # 垂直拖拽影响X轴旋转
|
|
|
+
|
|
|
+ # 限制垂直旋转范围,防止翻转
|
|
|
+ rotation_x = max(-90, min(90, rotation_x))
|
|
|
+
|
|
|
+ # 清除屏幕
|
|
|
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
|
|
+
|
|
|
+ # 重置模型视图矩阵并设置相机位置
|
|
|
+ glLoadIdentity()
|
|
|
+ glTranslatef(0.0, 0.0, camera_z) # 相机位置,可通过鼠标滚轮调整
|
|
|
+
|
|
|
+ # 应用鼠标控制的旋转(先应用手动旋转,再应用自动旋转)
|
|
|
+ glRotatef(rotation_x, 1.0, 0.0, 0.0) # 绕X轴旋转(上下视角)
|
|
|
+ glRotatef(rotation_y, 0.0, 1.0, 0.0) # 绕Y轴旋转(左右视角)
|
|
|
+ glRotatef(rotation_angle, 0.0, 1.0, 0.0) # 绕竖直中线/Y轴自动旋转
|
|
|
+
|
|
|
+ # 目录树可视化
|
|
|
+ ball_radius = 0.0667 # 节点球的半径(默认大小)
|
|
|
+
|
|
|
+ # 绘制所有连线(目录树边)
|
|
|
+ glDisable(GL_LIGHTING) # 禁用光照以使用纯色
|
|
|
+ glColor3f(1.0, 1.0, 1.0) # 白色连线
|
|
|
+ glLineWidth(2.0)
|
|
|
+
|
|
|
+ glBegin(GL_LINES)
|
|
|
+ for edge in tree_edges:
|
|
|
+ node1_idx, node2_idx = edge
|
|
|
+ x1, y1, z1 = node_positions[node1_idx]
|
|
|
+ x2, y2, z2 = node_positions[node2_idx]
|
|
|
+ glVertex3f(x1, y1, z1)
|
|
|
+ glVertex3f(x2, y2, z2)
|
|
|
+ glEnd()
|
|
|
+
|
|
|
+ # 绘制锥形表面(连接父文件夹与其子节点形成锥形)
|
|
|
+ glDisable(GL_LIGHTING) # 临时禁用光照以使用纯色
|
|
|
+ glColor4f(0.4, 0.4, 0.6, 0.3) # 半透明蓝色系,alpha值稍高以增强重叠效果
|
|
|
+
|
|
|
+ # 启用混合以实现透明效果,使重叠区域变暗
|
|
|
+ glEnable(GL_BLEND)
|
|
|
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
|
+
|
|
|
+ # 遍历所有节点,查找作为父节点的文件夹
|
|
|
+ for i, (name, is_dir, parent_id, depth, full_path) in enumerate(tree_nodes):
|
|
|
+ if is_dir: # 如果是文件夹节点
|
|
|
+ # 获取该文件夹的所有子节点
|
|
|
+ child_nodes = tree_children[i]
|
|
|
+ if len(child_nodes) > 0:
|
|
|
+ # 获取父节点坐标
|
|
|
+ parent_x, parent_y, parent_z = node_positions[i]
|
|
|
+
|
|
|
+ # 如果只有一个子节点,无法形成锥形,跳过
|
|
|
+ if len(child_nodes) < 2:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # 开始绘制三角形扇形来形成锥形表面
|
|
|
+ glBegin(GL_TRIANGLE_FAN)
|
|
|
+ glVertex3f(parent_x, parent_y, parent_z) # 锥形顶点(父节点)
|
|
|
+
|
|
|
+ # 按顺序连接子节点形成锥形侧面
|
|
|
+ for child_id in child_nodes:
|
|
|
+ child_x, child_y, child_z = node_positions[child_id]
|
|
|
+ glVertex3f(child_x, child_y, child_z)
|
|
|
+
|
|
|
+ # 闭合扇形,再次指定第一个子节点
|
|
|
+ if len(child_nodes) > 0:
|
|
|
+ first_child_x, first_child_y, first_child_z = node_positions[child_nodes[0]]
|
|
|
+ glVertex3f(first_child_x, first_child_y, first_child_z)
|
|
|
+
|
|
|
+ glEnd()
|
|
|
+
|
|
|
+ glDisable(GL_BLEND) # 禁用混合
|
|
|
+ glEnable(GL_LIGHTING) # 重新启用光照
|
|
|
+
|
|
|
+ # 绘制所有节点球(根据类型使用不同颜色)
|
|
|
+ for i, (name, is_dir, parent_id, depth, full_path) in enumerate(tree_nodes):
|
|
|
+ x, y, z = node_positions[i]
|
|
|
+
|
|
|
+ # 根据节点类型设置材质颜色(选中节点显示为红色)
|
|
|
+ if i in blinking_nodes: # 闪烁节点
|
|
|
+ # 计算闪烁因子(正弦波,周期约2秒)
|
|
|
+ blink_factor = (math.sin((current_time - blink_start_time) * 0.005) + 1) * 0.5
|
|
|
+ # 基础颜色 #c31c1f
|
|
|
+ base_r, base_g, base_b = 0.7647, 0.1098, 0.1216
|
|
|
+ # 亮度在0.7到1.0之间变化
|
|
|
+ brightness = 0.7 + 0.3 * blink_factor
|
|
|
+ color = [base_r * brightness, base_g * brightness, base_b * brightness, 1.0]
|
|
|
+ ambient = [base_r * 0.5, base_g * 0.5, base_b * 0.5, 1.0]
|
|
|
+ specular = [base_r * 0.8, base_g * 0.8, base_b * 0.8, 1.0]
|
|
|
+ elif i == selected_node_index: # 选中节点
|
|
|
+ color = [1.0, 0.0, 0.0, 1.0] # 红色
|
|
|
+ ambient = [0.5, 0.0, 0.0, 1.0]
|
|
|
+ specular = [0.8, 0.3, 0.3, 1.0]
|
|
|
+ elif i == 0: # 根节点
|
|
|
+ color = [1.0, 1.0, 0.0, 1.0] # 黄色
|
|
|
+ ambient = [0.5, 0.5, 0.0, 1.0]
|
|
|
+ specular = [0.8, 0.8, 0.4, 1.0]
|
|
|
+ elif is_dir: # 目录
|
|
|
+ color = [0.2902, 0.5961, 0.3098, 1.0] # #4a984f
|
|
|
+ ambient = [0.2, 0.4, 0.2, 1.0]
|
|
|
+ specular = [0.4, 0.8, 0.4, 1.0]
|
|
|
+ else: # 文件
|
|
|
+ color = [0.7059, 0.7176, 0.2549, 1.0] # #b4b741
|
|
|
+ ambient = [0.4, 0.4, 0.15, 1.0]
|
|
|
+ specular = [0.8, 0.8, 0.4, 1.0]
|
|
|
+
|
|
|
+ shininess = [30.0]
|
|
|
+
|
|
|
+ glMaterialfv(GL_FRONT, GL_AMBIENT, ambient)
|
|
|
+ glMaterialfv(GL_FRONT, GL_DIFFUSE, color)
|
|
|
+ glMaterialfv(GL_FRONT, GL_SPECULAR, specular)
|
|
|
+ glMaterialfv(GL_FRONT, GL_SHININESS, shininess)
|
|
|
+
|
|
|
+ glPushMatrix()
|
|
|
+ glTranslatef(x, y, z)
|
|
|
+ quad = gluNewQuadric()
|
|
|
+ gluQuadricNormals(quad, GLU_SMOOTH)
|
|
|
+ # 根据深度和节点类型调整球体大小:深度越大,球体越小;文件节点的半径缩小为原来的1/3,文件夹节点的半径缩小为原来的1/2
|
|
|
+ # 再次缩小球体大小:文件节点为现在的1/3,文件夹节点为现在的1/2
|
|
|
+ if is_dir:
|
|
|
+ current_radius = (ball_radius / 2) * 0.5 * (0.85 ** depth) # 文件夹节点的半径再缩小一半
|
|
|
+ else:
|
|
|
+ current_radius = (ball_radius / 3) * 0.33 * (0.85 ** depth) # 文件节点的半径再缩小为原来的1/3
|
|
|
+ gluSphere(quad, current_radius, 32, 32)
|
|
|
+ gluDeleteQuadric(quad)
|
|
|
+ glPopMatrix()
|
|
|
+
|
|
|
+ # 绘制文件名
|
|
|
+ if i in blinking_nodes: # 闪烁节点使用红色文本
|
|
|
+ draw_text(x, y + current_radius + 0.05, z, name, color=(1.0, 0.0, 0.0)) # 红色文本
|
|
|
+ else:
|
|
|
+ draw_text(x, y + current_radius + 0.05, z, name) # 默认白色文本
|
|
|
+
|
|
|
+ # 交换缓冲区
|
|
|
+ pygame.display.flip()
|
|
|
+ # 控制帧率 - 增加等待时间使动画更慢
|
|
|
+ pygame.time.wait(20)
|
|
|
+
|
|
|
+# 退出 Pygame
|
|
|
+pygame.quit()
|