From a65ce23ceeb970d61d9116618dadd328f282666a Mon Sep 17 00:00:00 2001 From: dmy Date: Tue, 3 Mar 2026 15:58:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20EGM=20=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E5=8A=A8=E7=94=BB=E5=8F=AF=E8=A7=86=E5=8C=96=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 web 界面中实现 EGM 计算过程的动画展示,包括地线保护弧、导线暴露弧和地面线的动态绘制。重构 main.py 以支持可选的动画参数传递,并新增 Animation.vue 组件和 WebAnimation 类实现前后端交互。 --- main.py | 36 ++-- webui/src/App.vue | 2 +- webui/src/components/Animation.vue | 258 +++++++++++++++++++++++++ webui/src/components/ParameterForm.vue | 5 + webview_app.py | 126 +++++++++++- 5 files changed, 409 insertions(+), 18 deletions(-) create mode 100644 webui/src/components/Animation.vue 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 @@ + + + + + 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 界面...")