# worm_gear_v2.py
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
from tkinter import ttk, messagebox
import math
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import stl.mesh as stlmesh # pip install numpy-stl
import time # 用于性能测量
# -------------------- 参数类 --------------------
class WormParams:
def __init__(self, module=2.0, teeth=2, length=50.0,
pressure_angle=20.0, helix_angle=5.0,
backlash=0.1, root_factor=1.0):
self.module = module
self.teeth = int(teeth)
self.length = length
self.pressure_angle = np.radians(pressure_angle)
self.helix_angle = np.radians(helix_angle)
self.backlash = backlash
self.root_factor = root_factor
self._derive()
def _derive(self):
"""计算蜗杆的派生参数"""
self.pitch_radius = self.module * self.teeth / 2
self.addendum = self.module
self.dedendum = 1.25 * self.module * self.root_factor
self.base_radius = self.pitch_radius * np.cos(self.pressure_angle)
self.lead = 2 * np.pi * self.pitch_radius / np.tan(self.helix_angle)
self.root_radius = self.pitch_radius - self.dedendum
self.tip_radius = self.pitch_radius + self.addendum
# 根据复杂度动态调整分辨率
complexity = max(1, min(5, int(50 / self.module)))
self.axial_res = 50 * complexity
self.circum_res = 60 * complexity
self.tooth_res = 50 * complexity
def validate(self):
"""验证参数的有效性"""
self.module = np.clip(self.module, 0.5, 10)
self.teeth = int(np.clip(self.teeth, 1, 8))
self.pressure_angle = np.radians(
np.clip(np.degrees(self.pressure_angle), 5, 35))
self.helix_angle = np.radians(
np.clip(np.degrees(self.helix_angle), 2, 20))
self.root_factor = np.clip(self.root_factor, 0.6, 1.4)
self._derive()
if self.base_radius > self.tip_radius:
self.pressure_angle = np.radians(20)
self._derive()
# -------------------- 几何生成 --------------------
def _involute(rb, r_end, n, side=1):
"""生成渐开线曲线"""
t_max = np.sqrt((r_end / rb) ** 2 - 1)
t = np.linspace(0, t_max, n)
x = rb * (np.cos(side * t) + t * np.sin(side * t))
y = rb * (np.sin(side * t) - t * np.cos(side * t))
return np.column_stack((x, y))
def generate_tooth_profile(p, n_points):
"""生成齿形轮廓"""
rb, ra, rf = p.base_radius, p.tip_radius, p.root_radius
# 生成右侧和左侧渐开线
prof_r = _involute(rb, ra, n_points // 2, 1)
prof_l = _involute(rb, ra, n_points // 2, -1)[::-1]
# 齿根弧
ang = np.linspace(-np.pi / 2 + p.pressure_angle,
np.pi / 2 - p.pressure_angle, n_points // 3)
root = np.column_stack((rf * np.cos(ang), rf * np.sin(ang)))
# 齿顶弧
ang = np.linspace(np.pi / 2 + p.pressure_angle,
3 * np.pi / 2 - p.pressure_angle, n_points // 3)
tip = np.column_stack((ra * np.cos(ang), ra * np.sin(ang)))
# 组合所有部分
profile = np.vstack((prof_r, tip, prof_l, root))
# 添加齿侧间隙
if p.backlash > 0:
gap = np.array([np.cos(p.pressure_angle),
np.sin(p.pressure_angle)]) * p.backlash / 2
profile[:len(prof_r)] -= gap
profile[len(prof_r) + len(tip):-len(root)] += gap
# 确保轮廓闭合
if not np.allclose(profile[0], profile[-1]):
profile = np.vstack([profile, profile[0]])
return profile
def build_worm_mesh(p):
"""构建蜗杆网格"""
try:
tooth = generate_tooth_profile(p, p.tooth_res)
n_prof = len(tooth)
z = np.linspace(0, p.length, p.axial_res)
phase = 2 * np.pi * z / p.lead
tooth_offset = 2 * np.pi * np.arange(p.teeth) / p.teeth
vertices = []
# 生成顶点
for i in range(p.axial_res):
rot = phase[i]
for j, pt in enumerate(tooth):
for k, off in enumerate(tooth_offset):
x = pt[0] * np.cos(rot + off) - pt[1] * np.sin(rot + off)
y = pt[0] * np.sin(rot + off) + pt[1] * np.cos(rot + off)
vertices.append([x, y, z[i]])
vertices = np.array(vertices)
# 生成面
faces = []
nV_slice = n_prof * p.teeth
for i in range(p.axial_res - 1):
for j in range(n_prof - 1):
for k in range(p.teeth):
idx = i * nV_slice + j * p.teeth + k
next_k = (k + 1) % p.teeth # 处理最后一个齿的连接
nxt = idx + nV_slice
# 创建两个三角形面片
faces.append([idx, idx + p.teeth, nxt])
faces.append([idx + p.teeth, nxt + p.teeth, nxt])
# 连接齿间间隙
if j == 0: # 齿根部分
faces.append([idx, nxt, idx + nV_slice - p.teeth + k])
faces.append([nxt, nxt + nV_slice - p.teeth + k, idx + nV_slice - p.teeth + k])
return vertices, np.array(faces)
except Exception as e:
print(f"构建网格时出错: {e}")
return np.array([]), np.array([])
def laplacian_smooth(vertices, faces, it=2, lam=0.5):
"""拉普拉斯平滑算法"""
from collections import defaultdict
# 构建邻接表
adj = defaultdict(set)
for tri in faces:
for i in tri:
adj[i].update(tri)
adj = {k: list(v) for k, v in adj.items()}
# 应用平滑
V = vertices.copy()
for _ in range(it):
for i, nei in adj.items():
if len(nei) < 3: # 避免边缘顶点过度平滑
continue
V[i] = (1 - lam) * vertices[i] + lam * np.mean(vertices[nei], axis=0)
vertices = V.copy()
return vertices
# -------------------- GUI --------------------
class WormVisualizer:
def __init__(self, master):
self.master = master
master.title("高精度蜗杆3D可视化 v2")
master.geometry("1000x700")
self.params = WormParams()
self.create_widgets()
self.create_3d_canvas()
self.last_mesh = None
self.is_rendering = False
# 初始渲染
self.master.after(100, self.render_worm)
def create_widgets(self):
"""创建GUI控件"""
main_frame = ttk.Frame(self.master)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 左侧参数面板
param_frame = ttk.LabelFrame(main_frame, text="参数控制", width=250)
param_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
param_frame.pack_propagate(False)
keys = [("模数", "module", 0.5, 10, 0.1),
("齿数(=头数)", "teeth", 1, 8, 1),
("长度/mm", "length", 20, 100, 1),
("压力角/°", "pressure_angle", 5, 35, 1),
("螺旋升角/°", "helix_angle", 2, 20, 1),
("齿侧间隙/mm", "backlash", 0.0, 0.5, 0.01),
("齿根圆系数", "root_factor", 0.6, 1.4, 0.05)]
self.vars = {}
self.labels = {}
for r, (lab, k, lo, hi, res) in enumerate(keys):
frame = ttk.Frame(param_frame)
frame.pack(fill=tk.X, pady=2)
ttk.Label(frame, text=lab, width=12).pack(side=tk.LEFT)
var = tk.DoubleVar(value=getattr(self.params, k))
spin = ttk.Spinbox(frame, from_=lo, to=hi, increment=res,
textvariable=var, width=8,
command=lambda k=k: self.param_changed(k))
spin.pack(side=tk.RIGHT)
# 显示当前值
val_label = ttk.Label(frame, text=f"{var.get():.2f}", width=6)
val_label.pack(side=tk.RIGHT, padx=5)
self.vars[k] = var
self.labels[k] = val_label
# 绑定事件
var.trace_add("write", lambda *args, k=k: self.update_label(k))
# 按钮框架
btn_frame = ttk.Frame(param_frame)
btn_frame.pack(fill=tk.X, pady=10)
ttk.Button(btn_frame, text="重新生成", command=self.render_worm).pack(side=tk.TOP, fill=tk.X, pady=2)
ttk.Button(btn_frame, text="导出 STL", command=self.export_stl).pack(side=tk.TOP, fill=tk.X, pady=2)
ttk.Button(btn_frame, text="重置参数", command=self.reset_params).pack(side=tk.TOP, fill=tk.X, pady=2)
# 状态栏
self.status_var = tk.StringVar(value="就绪")
status_bar = ttk.Label(param_frame, textvariable=self.status_var, relief=tk.SUNKEN)
status_bar.pack(side=tk.BOTTOM, fill=tk.X, pady=(10, 0))
def create_3d_canvas(self):
"""创建3D画布"""
# 右侧3D可视化面板
viz_frame = ttk.Frame(self.master)
viz_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
self.fig = plt.Figure(figsize=(8, 6), dpi=100)
self.ax = self.fig.add_subplot(111, projection='3d')
self.canvas = FigureCanvasTkAgg(self.fig, master=viz_frame)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# 添加工具栏
toolbar_frame = ttk.Frame(viz_frame)
toolbar_frame.pack(fill=tk.X)
ttk.Button(toolbar_frame, text="旋转", command=self.toggle_rotation).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="重置视图", command=self.reset_view).pack(side=tk.LEFT, padx=2)
self.rotation_enabled = False
self.rotation_angle = 0
def param_changed(self, param_name):
"""参数变化时的回调"""
value = self.vars[param_name].get()
setattr(self.params, param_name, value)
self.params.validate()
self.update_label(param_name)
def update_label(self, param_name):
"""更新参数标签"""
value = self.vars[param_name].get()
if param_name in ["pressure_angle", "helix_angle"]:
value = np.degrees(value)
self.labels[param_name].config(text=f"{value:.2f}")
def reset_params(self):
"""重置参数到默认值"""
default_params = WormParams()
for k, var in self.vars.items():
var.set(getattr(default_params, k))
self.render_worm()
def toggle_rotation(self):
"""切换自动旋转"""
self.rotation_enabled = not self.rotation_enabled
if self.rotation_enabled:
self.rotate_view()
def rotate_view(self):
"""自动旋转视图"""
if self.rotation_enabled:
self.rotation_angle = (self.rotation_angle + 2) % 360
self.ax.view_init(elev=20, azim=self.rotation_angle)
self.canvas.draw()
self.master.after(50, self.rotate_view)
def reset_view(self):
"""重置视图"""
self.ax.view_init(elev=20, azim=0)
self.canvas.draw()
def render_worm(self):
"""渲染蜗杆模型"""
if self.is_rendering:
return
self.is_rendering = True
self.status_var.set("正在生成模型...")
self.master.update()
try:
start_time = time.time()
# 更新参数
for k, var in self.vars.items():
setattr(self.params, k, var.get())
self.params.validate()
# 清除当前图形
self.ax.clear()
# 生成网格
verts, faces = build_worm_mesh(self.params)
if len(verts) == 0 or len(faces) == 0:
messagebox.showerror("错误", "无法生成蜗杆模型,请检查参数")
return
# 应用平滑
verts = laplacian_smooth(verts, faces)
self.last_mesh = (verts, faces)
# 降采样以提高性能
step = max(1, len(verts) // 20000)
sample_verts = verts[::step]
# 绘制模型
mesh = Poly3DCollection(sample_verts[faces], alpha=0.85, linewidth=0.1)
mesh.set_facecolor(plt.cm.viridis(np.linspace(0, 1, len(faces))))
mesh.set_edgecolor('gray')
self.ax.add_collection3d(mesh)
# 设置坐标轴范围
lim = np.array([verts[:, i].ptp() for i in range(3)]).max() / 2
cen = verts.mean(axis=0)
for i in range(3):
self.ax.set_xlim3d(cen[0]-lim, cen[0]+lim)
self.ax.set_ylim3d(cen[1]-lim, cen[1]+lim)
self.ax.set_zlim3d(cen[2]-lim, cen[2]+lim)
self.ax.set_xlabel('X/mm')
self.ax.set_ylabel('Y/mm')
self.ax.set_zlabel('Z/mm')
self.ax.set_title(f"蜗杆3D模型 (生成时间: {time.time()-start_time:.2f}秒)")
self.canvas.draw()
self.status_var.set(f"就绪 - 顶点: {len(verts)}, 面: {len(faces)}")
except Exception as e:
messagebox.showerror("错误", f"生成模型时出错: {str(e)}")
self.status_var.set("错误")
finally:
self.is_rendering = False
def export_stl(self):
"""导出STL文件"""
if self.last_mesh is None:
messagebox.showwarning("警告", "没有可导出的模型")
return
try:
self.status_var.set("正在导出STL...")
self.master.update()
# 弹出文件选择对话框
from tkinter import filedialog
filename = filedialog.asksaveasfilename(
defaultextension=".stl",
filetypes=[("STL文件", "*.stl"), ("所有文件", "*.*")],
title="保存STL文件"
)
if filename:
export_stl(*self.last_mesh, filename)
messagebox.showinfo("完成", f"已成功导出到 {filename}")
self.status_var.set("STL导出完成")
except Exception as e:
messagebox.showerror("错误", f"导出STL时出错: {str(e)}")
self.status_var.set("导出失败")
# -------------------- STL 导出 --------------------
def export_stl(vertices, faces, filename="worm.stl"):
"""导出STL文件"""
mesh = stlmesh.Mesh(np.zeros(faces.shape[0], dtype=stlmesh.Mesh.dtype))
for i, f in enumerate(faces):
for j in range(3):
mesh.vectors[i][j] = vertices[f[j], :]
mesh.save(filename)
# -------------------- main --------------------
if __name__ == "__main__":
root = tk.Tk()
app = WormVisualizer(root)
root.mainloop()
最新发布