diff --git a/main.py b/main.py
index 528f882..2245aaa 100644
--- a/main.py
+++ b/main.py
@@ -6,7 +6,6 @@ import tomli
from loguru import logger
from core import *
import timeit
-from animation import Animation
# 打印参数
@@ -62,11 +61,12 @@ def read_parameter(toml_file_path) -> Parameter:
return para
-def run_egm(para: Parameter) -> dict:
+def run_egm(para: Parameter, animation=None) -> dict:
"""
执行 EGM 计算的核心函数,可被外部调用。
Args:
para: 参数对象,包含所有计算所需的参数。
+ animation: 可选的动画对象,用于可视化。需要实现 add_rs, add_rc, add_rg_line, add_expose_area, pause 方法。
Returns:
计算结果字典。
"""
@@ -104,9 +104,10 @@ def run_egm(para: Parameter) -> dict:
ng = func_ng(td, para.ng)
avr_n_sf = 0 # 考虑电压的影响计算的跳闸率
ground_angels = para.ground_angels
- # 初始化动画
- animate = Animation()
- animate.enable(False)
+ # 动画对象:如果传入了 animation 则使用,否则不启用动画
+ animate = animation
+ if animate:
+ animate.enable(True) # 启用动画
# animate.show()
for ground_angel in ground_angels:
logger.info(f"地面倾角{ground_angel/math.pi*180:.3f}°")
@@ -179,14 +180,17 @@ def run_egm(para: Parameter) -> dict:
): # 雷电流
logger.info(f"尝试计算电流为{i_bar:.2f}")
rs = rs_fun(i_bar)
- animate.add_rs(rs, rs_x, rs_y)
+ if animate:
+ animate.add_rs(rs, rs_x, rs_y)
rc = rc_fun(i_bar, u_ph)
- animate.add_rc(rc, rc_x, rc_y)
+ if animate:
+ animate.add_rc(rc, rc_x, rc_y)
rg = rg_fun(i_bar, rc_y, u_ph, typ=rg_type)
rg_line_func = None
if rg_type == "g":
rg_line_func = rg_line_function_factory(rg, ground_angel)
- animate.add_rg_line(rg_line_func)
+ if animate:
+ animate.add_rg_line(rg_line_func)
rs_rc_circle_intersection = solve_circle_intersection(
rs, rc, rs_x, rs_y, rc_x, rc_y
)
@@ -219,12 +223,13 @@ def run_egm(para: Parameter) -> dict:
"上面的导地线无法保护下面的导地线,检查设置参数。"
)
continue
- animate.add_expose_area(
- rc_x,
- rc_y,
- *rs_rc_circle_intersection,
- *circle_rc_or_rg_line_intersection,
- )
+ if animate:
+ animate.add_expose_area(
+ rc_x,
+ rc_y,
+ *rs_rc_circle_intersection,
+ *circle_rc_or_rg_line_intersection,
+ )
cad = Draw()
cad.draw(
i_min,
@@ -298,7 +303,8 @@ def run_egm(para: Parameter) -> dict:
logger.info(f"电流为{i_bar}kA时,暴露弧已经完全被屏蔽")
exposed_curve_shielded = True
break
- animate.pause()
+ if animate:
+ animate.pause()
# 判断是否导线已经被完全保护
if abs(i_max - _max_i) < 1e-5:
logger.info("无法找到最大电流,可能是杆塔较高。")
diff --git a/webui/src/App.vue b/webui/src/App.vue
index 18d9f60..93c41e2 100644
--- a/webui/src/App.vue
+++ b/webui/src/App.vue
@@ -4,4 +4,4 @@
\ No newline at end of file
+
diff --git a/webui/src/components/Animation.vue b/webui/src/components/Animation.vue
new file mode 100644
index 0000000..fe02ecb
--- /dev/null
+++ b/webui/src/components/Animation.vue
@@ -0,0 +1,258 @@
+
+
+
+
+
+ EGM 动画可视化
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webui/src/components/ParameterForm.vue b/webui/src/components/ParameterForm.vue
index 2294376..555377d 100644
--- a/webui/src/components/ParameterForm.vue
+++ b/webui/src/components/ParameterForm.vue
@@ -351,6 +351,9 @@
+
+
+
@@ -361,6 +364,7 @@
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import type { AllParameters } from '@/types'
import LogComponent from './Log.vue'
+import Animation from './Animation.vue'
// 默认参数
const defaultParams: AllParameters = {
@@ -394,6 +398,7 @@ const calculating = ref(false)
const result = ref<{ tripping_rate: number; n_sf_phases: number[]; message: string } | null>(null)
const error = ref(null)
const logRef = ref | null>(null)
+const animationRef = ref | null>(null)
const fileInput = ref(null)
// 雷电流概率密度系数设置开关
const showIpCoefficients = ref(false)
diff --git a/webview_app.py b/webview_app.py
index c76ae51..3229b21 100644
--- a/webview_app.py
+++ b/webview_app.py
@@ -23,6 +23,126 @@ from core import Parameter
from main import parameter_display, run_egm
+class WebAnimation:
+ """
+ Web 动画类,将 Python 端的 Animation 调用映射到前端 JavaScript
+ 对应 Animation.vue 的功能
+ """
+
+ def __init__(self, window=None):
+ self._window = window
+ self._disable = True # 默认禁用
+
+ def set_window(self, window):
+ """设置窗口对象"""
+ self._window = window
+
+ def enable(self, enabled: bool):
+ """
+ 启用/禁用动画
+ 对应 animation.py 的 enable 方法和 Animation.vue 的 enable 方法
+ """
+ self._disable = not enabled
+ if self._window:
+ js_code = f'if(window.animationApi){{window.animationApi.enable({str(enabled).lower()})}}'
+ self._window.evaluate_js(js_code)
+
+ def init_fig(self):
+ """初始化画布"""
+ if self._disable or not self._window:
+ return
+ js_code = 'if(window.animationApi){window.animationApi.initFig()}'
+ self._window.evaluate_js(js_code)
+
+ def add_rs(self, rs: float, rs_x: float, rs_y: float):
+ """
+ 添加地线保护弧(RS 圆)
+ 对应 animation.py 的 add_rs 方法
+ """
+ if self._disable or not self._window:
+ return
+ js_code = f'if(window.animationApi){{window.animationApi.addRs({rs}, {rs_x}, {rs_y})}}'
+ self._window.evaluate_js(js_code)
+
+ def add_rc(self, rc: float, rc_x: float, rc_y: float):
+ """
+ 添加导线暴露弧(RC 圆)
+ 对应 animation.py 的 add_rc 方法
+ """
+ if self._disable or not self._window:
+ return
+ js_code = f'if(window.animationApi){{window.animationApi.addRc({rc}, {rc_x}, {rc_y})}}'
+ self._window.evaluate_js(js_code)
+
+ def add_rg_line(self, line_func):
+ """
+ 添加地面线(RG 线)
+ 对应 animation.py 的 add_rg_line 方法
+
+ Args:
+ line_func: 一个函数,接收 x 返回 y
+ """
+ if self._disable or not self._window:
+ return
+ # 生成线上的点,传递给前端
+ # 由于无法直接传递函数,我们预先计算一些点
+ import numpy as np
+ x_points = np.linspace(0, 300, 50)
+ y_points = [line_func(x) for x in x_points]
+ points = list(zip(x_points.tolist(), y_points))
+
+ js_code = f'''
+ if(window.animationApi){{
+ window.animationApi.addRgLine({json.dumps(points)})
+ }}
+ '''
+ self._window.evaluate_js(js_code)
+
+ def add_expose_area(
+ self,
+ rc_x: float,
+ rc_y: float,
+ intersection_x1: float,
+ intersection_y1: float,
+ intersection_x2: float,
+ intersection_y2: float
+ ):
+ """
+ 添加暴露弧区域(两条红线)
+ 对应 animation.py 的 add_expose_area 方法
+ """
+ if self._disable or not self._window:
+ return
+ js_code = f'''if(window.animationApi){{
+ window.animationApi.addExposeArea(
+ {rc_x}, {rc_y},
+ {intersection_x1}, {intersection_y1},
+ {intersection_x2}, {intersection_y2}
+ )
+ }}'''
+ self._window.evaluate_js(js_code)
+
+ def clear(self):
+ """清除画布"""
+ if self._disable or not self._window:
+ return
+ js_code = 'if(window.animationApi){window.animationApi.clear()}'
+ self._window.evaluate_js(js_code)
+
+ def pause(self):
+ """
+ 暂停并刷新
+ 对应 animation.py 的 pause 方法
+ """
+ if self._disable or not self._window:
+ return
+ js_code = 'if(window.animationApi){window.animationApi.pause()}'
+ self._window.evaluate_js(js_code)
+ # 添加延迟以便动画可见
+ import time
+ time.sleep(0.1) # 增加延迟,让用户看清动画
+
+
class WebHandler:
"""Web日志处理器"""
def __init__(self, callback=None):
@@ -86,6 +206,7 @@ class EGMWebApp:
self._loguru_handler_id = None
self._log_queue: queue.Queue = queue.Queue()
self._running = False
+ self.animation = WebAnimation() # Web 动画实例
def _process_log_queue(self):
"""处理日志队列,在主线程中定时调用"""
@@ -220,8 +341,8 @@ class EGMWebApp:
logger.info("开始执行 EGM 计算...")
- # 调用 main.py 的核心计算函数
- result = run_egm(para)
+ # 调用 main.py 的核心计算函数,传递 animation 对象
+ result = run_egm(para, self.animation)
self.add_log("info", "EGM 计算完成")
@@ -467,6 +588,7 @@ def start_webview():
# 将窗口对象传递给 API
api.window = window
+ api.animation.set_window(window)
# 启动
logger.info("启动 EGM Web 界面...")