ball-demo.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. import pygame
  2. import math
  3. import time
  4. import os
  5. try:
  6. import pyperclip
  7. clipboard_available = True
  8. except ImportError:
  9. clipboard_available = False
  10. print("警告: pyperclip 模块未安装,剪贴板功能不可用。请运行 'pip install pyperclip' 安装。")
  11. from pygame.locals import *
  12. from OpenGL.GL import *
  13. from OpenGL.GLU import *
  14. # 初始化字体
  15. pygame.font.init()
  16. # 使用默认字体,增大字体大小
  17. font = pygame.font.Font(None, 24)
  18. # 模拟模式开关(True: 使用模拟数据,False: 扫描真实目录)
  19. # USE_MOCK_DATA = True
  20. USE_MOCK_DATA = False
  21. # 初始化 Pygame
  22. pygame.init()
  23. # 获取屏幕尺寸
  24. info = pygame.display.Info()
  25. screen_width = info.current_w
  26. screen_height = info.current_h
  27. # 设置窗口大小(高度为屏幕高度的80%,保持原始宽高比)
  28. height = int(screen_height * 0.8)
  29. width = int(height * (800 / 600)) # 保持原始宽高比 800:600
  30. display = pygame.display.set_mode((width, height), DOUBLEBUF | OPENGL)
  31. pygame.display.set_caption('3D Yellow Ball')
  32. # 启用深度测试
  33. glEnable(GL_DEPTH_TEST)
  34. # 设置背景色
  35. glClearColor(0.1, 0.1, 0.1, 1.0)
  36. # 禁用混合,确保完全不透明
  37. glDisable(GL_BLEND)
  38. # 设置光照
  39. glEnable(GL_LIGHTING)
  40. glEnable(GL_LIGHT0)
  41. # 设置光源位置
  42. light_position = [2.0, 2.0, 2.0, 1.0] # 点光源
  43. glLightfv(GL_LIGHT0, GL_POSITION, light_position)
  44. # 设置光源颜色
  45. light_ambient = [0.3, 0.3, 0.3, 1.0]
  46. light_diffuse = [1.0, 1.0, 1.0, 1.0]
  47. light_specular = [1.0, 1.0, 1.0, 1.0]
  48. glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient)
  49. glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse)
  50. glLightfv(GL_LIGHT0, GL_SPECULAR, light_specular)
  51. # 设置投影矩阵(透视投影)
  52. glMatrixMode(GL_PROJECTION)
  53. glLoadIdentity()
  54. gluPerspective(45, (width/height), 0.1, 50.0)
  55. # 设置模型视图矩阵
  56. glMatrixMode(GL_MODELVIEW)
  57. glLoadIdentity()
  58. # 移动相机(现在在主循环中每帧设置)
  59. # glTranslatef(0.0, 0.0, -5)
  60. def generate_mock_tree():
  61. # 生成模拟目录树数据
  62. nodes = [
  63. ("root", True, -1, 0, "/mock/root"),
  64. ("dir1", True, 0, 1, "/mock/root/dir1"),
  65. ("dir2", True, 0, 1, "/mock/root/dir2"),
  66. ("file1.txt", False, 0, 1, "/mock/root/file1.txt"),
  67. ("file2.py", False, 0, 1, "/mock/root/file2.py"),
  68. ("subdir1", True, 1, 2, "/mock/root/dir1/subdir1"),
  69. ("file3.js", False, 1, 2, "/mock/root/dir1/file3.js"),
  70. ("file4.md", False, 2, 2, "/mock/root/dir2/file4.md"),
  71. ]
  72. edges = [
  73. (0, 1), (0, 2), (0, 3), (0, 4),
  74. (1, 5), (1, 6),
  75. (2, 7),
  76. ]
  77. # 构建子节点列表
  78. children = [[] for _ in range(len(nodes))]
  79. for parent_id, child_id in edges:
  80. children[parent_id].append(child_id)
  81. # 计算节点位置(使用原有算法)
  82. positions = []
  83. for i in range(len(nodes)):
  84. positions.append([0.0, 0.0, 0.0])
  85. # 计算最大深度
  86. max_depth_found = 0
  87. for node in nodes:
  88. max_depth_found = max(max_depth_found, node[3])
  89. # 递归计算位置函数
  90. def calculate_positions(node_id, start_angle, end_angle, depth):
  91. if depth > max_depth_found:
  92. return
  93. if node_id == 0: # 根节点
  94. positions[node_id] = [0.0, max_depth_found * 0.4, 0.0]
  95. else:
  96. parent_id = nodes[node_id][2]
  97. if parent_id >= 0:
  98. child_index = children[parent_id].index(node_id)
  99. num_siblings = len(children[parent_id])
  100. angle = start_angle + (end_angle - start_angle) * (child_index / max(num_siblings, 1))
  101. radius = 0.75 * (0.75 ** depth)
  102. px, py, pz = positions[parent_id]
  103. x = px + radius * math.cos(angle)
  104. y = py - 0.65
  105. z = pz + radius * math.sin(angle)
  106. positions[node_id] = [x, y, z]
  107. node_children = children[node_id]
  108. if node_children:
  109. angle_range = math.pi * 1.5
  110. angle_start = -angle_range / 2
  111. angle_step = angle_range / len(node_children)
  112. for i, child_id in enumerate(node_children):
  113. child_angle_start = angle_start + i * angle_step
  114. child_angle_end = angle_start + (i + 1) * angle_step
  115. calculate_positions(child_id, child_angle_start, child_angle_end, depth + 1)
  116. calculate_positions(0, -math.pi, math.pi, 0)
  117. return nodes, edges, positions, children
  118. def build_directory_tree(root_path, max_depth=3, max_children_per_node=30):
  119. # 如果启用模拟模式,返回模拟数据
  120. if USE_MOCK_DATA:
  121. return generate_mock_tree()
  122. # 递归扫描目录树
  123. nodes = [] # 每个节点: (name, is_dir, parent_id, depth)
  124. edges = [] # (parent_id, child_id)
  125. # 递归扫描函数
  126. def scan_directory(current_path, parent_id, depth):
  127. if depth >= max_depth:
  128. return
  129. try:
  130. entries = os.listdir(current_path)
  131. except Exception:
  132. return # 忽略所有访问错误
  133. # 过滤掉一些不需要的目录
  134. filtered_entries = []
  135. for entry in entries:
  136. # 过滤常见的版本控制和IDE目录
  137. if entry in ['.git', '.vscode', '.idea', 'node_modules', '__pycache__']:
  138. continue
  139. filtered_entries.append(entry)
  140. # 限制每个节点的最大子项数量
  141. filtered_entries = filtered_entries[:max_children_per_node]
  142. # 当前目录的节点ID:根节点为0,其他目录为parent_id
  143. current_node_id = 0 if parent_id == -1 else parent_id
  144. for entry in filtered_entries:
  145. entry_path = os.path.join(current_path, entry)
  146. is_dir = os.path.isdir(entry_path)
  147. # 添加子节点
  148. child_id = len(nodes)
  149. nodes.append((entry, is_dir, current_node_id, depth + 1, entry_path))
  150. edges.append((current_node_id, child_id))
  151. # 如果是目录,递归扫描
  152. if is_dir:
  153. scan_directory(entry_path, child_id, depth + 1)
  154. # 添加根节点
  155. root_name = os.path.basename(root_path.rstrip('\\'))
  156. nodes.append((root_name, True, -1, 0, root_path))
  157. # 从根节点开始递归扫描
  158. scan_directory(root_path, -1, 0)
  159. # 计算节点位置
  160. positions = []
  161. for i in range(len(nodes)):
  162. positions.append([0.0, 0.0, 0.0]) # 初始位置
  163. # 构建子节点列表
  164. children = [[] for _ in range(len(nodes))]
  165. for edge in edges:
  166. parent_id, child_id = edge
  167. children[parent_id].append(child_id)
  168. # 计算最大深度
  169. max_depth_found = 0
  170. for node in nodes:
  171. max_depth_found = max(max_depth_found, node[3])
  172. # 递归计算位置
  173. def calculate_positions(node_id, start_angle, end_angle, depth):
  174. if depth > max_depth_found:
  175. return
  176. # 计算当前节点位置
  177. if node_id == 0: # 根节点
  178. positions[node_id] = [0.0, max_depth_found * 0.4, 0.0] # 顶部,高度减半
  179. else:
  180. parent_id = nodes[node_id][2]
  181. if parent_id >= 0:
  182. # 子节点围绕父节点在圆上分布
  183. child_index = children[parent_id].index(node_id)
  184. num_siblings = len(children[parent_id])
  185. # 计算角度(在父节点周围的圆上)
  186. angle = start_angle + (end_angle - start_angle) * (child_index / max(num_siblings, 1))
  187. # 半径随深度减小
  188. radius = 0.75 * (0.75 ** depth) # 深度越大,半径越小,基础半径减半
  189. # 计算位置
  190. px, py, pz = positions[parent_id]
  191. x = px + radius * math.cos(angle)
  192. y = py - 0.65 # 每层向下移动,垂直距离减半
  193. z = pz + radius * math.sin(angle)
  194. positions[node_id] = [x, y, z]
  195. # 为子节点递归计算位置
  196. node_children = children[node_id]
  197. if node_children:
  198. # 计算子节点的角度范围
  199. angle_range = math.pi * 1.5 # 270度范围
  200. angle_start = -angle_range / 2
  201. angle_step = angle_range / len(node_children)
  202. for i, child_id in enumerate(node_children):
  203. child_angle_start = angle_start + i * angle_step
  204. child_angle_end = angle_start + (i + 1) * angle_step
  205. calculate_positions(child_id, child_angle_start, child_angle_end, depth + 1)
  206. # 从根节点开始计算位置
  207. calculate_positions(0, -math.pi, math.pi, 0)
  208. return nodes, edges, positions, children
  209. # 构建目录树(使用当前目录)
  210. # root_path = "."
  211. root_path = "E:\\agricultural_research_platform"
  212. tree_nodes, tree_edges, node_positions, tree_children = build_directory_tree(root_path)
  213. def draw_text(x, y, z, text):
  214. """在3D空间中绘制文本"""
  215. glDisable(GL_LIGHTING)
  216. glColor3f(1.0, 1.0, 1.0) # 白色文本
  217. # 尝试使用系统字体
  218. try:
  219. # 尝试使用 Arial 字体
  220. font = pygame.font.SysFont("Arial", 24)
  221. except:
  222. # 如果 Arial 不可用,使用默认字体
  223. font = pygame.font.Font(None, 24)
  224. # 渲染文本到 surface
  225. text_surface = font.render(text, True, (255, 255, 255), (0, 0, 0, 0))
  226. text_data = pygame.image.tostring(text_surface, "RGBA", True)
  227. # 创建纹理
  228. texture_id = glGenTextures(1)
  229. glBindTexture(GL_TEXTURE_2D, texture_id)
  230. # 设置纹理参数
  231. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
  232. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
  233. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
  234. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
  235. # 加载纹理数据
  236. glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, text_surface.get_width(), text_surface.get_height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, text_data)
  237. # 保存当前矩阵
  238. glPushMatrix()
  239. # 移动到指定位置
  240. glTranslatef(x, y, z)
  241. # 旋转文本,使其面向相机
  242. glRotatef(rotation_angle, 0.0, 1.0, 0.0)
  243. # 增大文本的缩放比例,使文本更清晰
  244. scale = 0.01
  245. glScalef(scale, scale, scale)
  246. # 启用纹理
  247. glEnable(GL_TEXTURE_2D)
  248. glBindTexture(GL_TEXTURE_2D, texture_id)
  249. # 启用混合,确保透明度正确显示
  250. glEnable(GL_BLEND)
  251. glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
  252. # 绘制四边形
  253. glBegin(GL_QUADS)
  254. glTexCoord2f(0, 1)
  255. glVertex3f(-text_surface.get_width()/2, -text_surface.get_height()/2, 0)
  256. glTexCoord2f(1, 1)
  257. glVertex3f(text_surface.get_width()/2, -text_surface.get_height()/2, 0)
  258. glTexCoord2f(1, 0)
  259. glVertex3f(text_surface.get_width()/2, text_surface.get_height()/2, 0)
  260. glTexCoord2f(0, 0)
  261. glVertex3f(-text_surface.get_width()/2, text_surface.get_height()/2, 0)
  262. glEnd()
  263. # 禁用混合
  264. glDisable(GL_BLEND)
  265. # 禁用纹理
  266. glDisable(GL_TEXTURE_2D)
  267. # 恢复矩阵
  268. glPopMatrix()
  269. glEnable(GL_LIGHTING)
  270. # 删除纹理
  271. glDeleteTextures([texture_id])
  272. # 初始化选中节点(根节点)
  273. selected_node_index = 0
  274. # 剪贴板相关变量
  275. clipboard_content = ""
  276. clipboard_check_time = 0
  277. blinking_nodes = set()
  278. file_content_cache = {}
  279. blink_start_time = pygame.time.get_ticks()
  280. # 旋转控制变量
  281. rotation_angle = 0.0
  282. # 主循环
  283. running = True
  284. while running:
  285. # 获取当前时间(用于闪烁效果)
  286. current_time = pygame.time.get_ticks()
  287. # 更新旋转角度(每10秒旋转一圈)
  288. rotation_angle = (current_time * 0.036) % 360.0
  289. # 检查剪贴板内容是否变化(如果剪贴板功能可用)
  290. if clipboard_available:
  291. if current_time - clipboard_check_time > 1000: # 每秒检查一次
  292. clipboard_check_time = current_time
  293. try:
  294. new_clipboard_content = pyperclip.paste()
  295. if new_clipboard_content != clipboard_content:
  296. clipboard_content = new_clipboard_content
  297. # 剪贴板内容变化,重新检查所有文件节点
  298. blinking_nodes.clear()
  299. if clipboard_content.strip(): # 剪贴板非空
  300. for i, (name, is_dir, parent_id, depth, full_path) in enumerate(tree_nodes):
  301. if not is_dir: # 文件节点
  302. # 检查文件内容是否包含剪贴板文本
  303. try:
  304. # 从缓存读取或加载文件内容
  305. if i not in file_content_cache:
  306. with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
  307. file_content_cache[i] = f.read()
  308. file_content = file_content_cache[i]
  309. if clipboard_content in file_content:
  310. blinking_nodes.add(i)
  311. except Exception as e:
  312. pass # 忽略读取错误
  313. except Exception as e:
  314. pass # 忽略剪贴板访问错误
  315. # 处理事件
  316. for event in pygame.event.get():
  317. if event.type == pygame.QUIT:
  318. running = False
  319. elif event.type == pygame.KEYDOWN:
  320. # 获取当前节点信息
  321. current_node = tree_nodes[selected_node_index]
  322. current_parent_id = current_node[2]
  323. current_depth = current_node[3]
  324. if event.key == pygame.K_UP: # 上键:移动到父节点
  325. if current_parent_id >= 0: # 有父节点
  326. selected_node_index = current_parent_id
  327. elif event.key == pygame.K_DOWN: # 下键:移动到第一个子节点
  328. if tree_children[selected_node_index]: # 有子节点
  329. selected_node_index = tree_children[selected_node_index][0]
  330. elif event.key == pygame.K_LEFT: # 左键:移动到前一个兄弟节点
  331. if current_parent_id >= 0: # 有父节点
  332. siblings = tree_children[current_parent_id]
  333. if len(siblings) > 1:
  334. current_index_in_siblings = siblings.index(selected_node_index)
  335. # 移动到前一个兄弟,如果当前是第一个则移动到最后一个
  336. new_index = (current_index_in_siblings - 1) % len(siblings)
  337. selected_node_index = siblings[new_index]
  338. elif event.key == pygame.K_RIGHT: # 右键:移动到后一个兄弟节点
  339. if current_parent_id >= 0: # 有父节点
  340. siblings = tree_children[current_parent_id]
  341. if len(siblings) > 1:
  342. current_index_in_siblings = siblings.index(selected_node_index)
  343. # 移动到后一个兄弟,如果当前是最后一个则移动到第一个
  344. new_index = (current_index_in_siblings + 1) % len(siblings)
  345. selected_node_index = siblings[new_index]
  346. # 清除屏幕
  347. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  348. # 重置模型视图矩阵并设置相机位置
  349. glLoadIdentity()
  350. glTranslatef(0.0, 0.0, -5) # 相机向后移动
  351. glRotatef(rotation_angle, 0.0, 1.0, 0.0) # 绕竖直中线/Y轴旋转
  352. # 目录树可视化
  353. ball_radius = 0.0667 # 节点球的半径(原来的1/3)
  354. # 绘制所有连线(目录树边)
  355. glDisable(GL_LIGHTING) # 禁用光照以使用纯色
  356. glColor3f(1.0, 1.0, 1.0) # 白色连线
  357. glLineWidth(2.0)
  358. glBegin(GL_LINES)
  359. for edge in tree_edges:
  360. node1_idx, node2_idx = edge
  361. x1, y1, z1 = node_positions[node1_idx]
  362. x2, y2, z2 = node_positions[node2_idx]
  363. glVertex3f(x1, y1, z1)
  364. glVertex3f(x2, y2, z2)
  365. glEnd()
  366. glEnable(GL_LIGHTING) # 重新启用光照
  367. # 绘制所有节点球(根据类型使用不同颜色)
  368. for i, (name, is_dir, parent_id, depth, full_path) in enumerate(tree_nodes):
  369. x, y, z = node_positions[i]
  370. # 根据节点类型设置材质颜色(选中节点显示为红色)
  371. if i in blinking_nodes: # 闪烁节点
  372. # 计算闪烁因子(正弦波,周期约2秒)
  373. blink_factor = (math.sin((current_time - blink_start_time) * 0.005) + 1) * 0.5
  374. # 基础颜色 #c31c1f
  375. base_r, base_g, base_b = 0.7647, 0.1098, 0.1216
  376. # 亮度在0.7到1.0之间变化
  377. brightness = 0.7 + 0.3 * blink_factor
  378. color = [base_r * brightness, base_g * brightness, base_b * brightness, 1.0]
  379. ambient = [base_r * 0.5, base_g * 0.5, base_b * 0.5, 1.0]
  380. specular = [base_r * 0.8, base_g * 0.8, base_b * 0.8, 1.0]
  381. elif i == selected_node_index: # 选中节点
  382. color = [1.0, 0.0, 0.0, 1.0] # 红色
  383. ambient = [0.5, 0.0, 0.0, 1.0]
  384. specular = [0.8, 0.3, 0.3, 1.0]
  385. elif i == 0: # 根节点
  386. color = [1.0, 1.0, 0.0, 1.0] # 黄色
  387. ambient = [0.5, 0.5, 0.0, 1.0]
  388. specular = [0.8, 0.8, 0.4, 1.0]
  389. elif is_dir: # 目录
  390. color = [0.2902, 0.5961, 0.3098, 1.0] # #4a984f
  391. ambient = [0.2, 0.4, 0.2, 1.0]
  392. specular = [0.4, 0.8, 0.4, 1.0]
  393. else: # 文件
  394. color = [0.7059, 0.7176, 0.2549, 1.0] # #b4b741
  395. ambient = [0.4, 0.4, 0.15, 1.0]
  396. specular = [0.8, 0.8, 0.4, 1.0]
  397. shininess = [30.0]
  398. glMaterialfv(GL_FRONT, GL_AMBIENT, ambient)
  399. glMaterialfv(GL_FRONT, GL_DIFFUSE, color)
  400. glMaterialfv(GL_FRONT, GL_SPECULAR, specular)
  401. glMaterialfv(GL_FRONT, GL_SHININESS, shininess)
  402. glPushMatrix()
  403. glTranslatef(x, y, z)
  404. quad = gluNewQuadric()
  405. gluQuadricNormals(quad, GLU_SMOOTH)
  406. # 根据深度调整球体大小:深度越大,球体越小
  407. current_radius = ball_radius * (0.85 ** depth)
  408. gluSphere(quad, current_radius, 32, 32)
  409. gluDeleteQuadric(quad)
  410. glPopMatrix()
  411. # 绘制文件名
  412. draw_text(x, y + current_radius + 0.05, z, name)
  413. # 交换缓冲区
  414. pygame.display.flip()
  415. # 控制帧率
  416. pygame.time.wait(10)
  417. # 退出 Pygame
  418. pygame.quit()