保存模型子级模型位置移动
This commit is contained in:
parent
4635458300
commit
74b6a3307c
@ -261,7 +261,7 @@ class SceneManagerIOMixin:
|
||||
all_nodes.extend(self.Spotlight)
|
||||
all_nodes.extend(self.Pointlight)
|
||||
|
||||
# SSBO模式下先把运行时编辑后的顶层变换同步回source_model_root,
|
||||
# SSBO模式下先把运行时编辑后的层级变换同步回source_model_root,
|
||||
# 再从source树保存,避免把chunk_*运行时结构写入scene.bam。
|
||||
ssbo_editor = getattr(self.world, "ssbo_editor", None)
|
||||
if ssbo_editor:
|
||||
|
||||
@ -407,75 +407,78 @@ class SSBOEditor:
|
||||
return list(root_node.get("children", []))
|
||||
|
||||
def _snapshot_top_level_transforms_to_source_root(self):
|
||||
"""Persist current top-level imported model transforms back into the source scene root."""
|
||||
"""Persist current runtime transforms back into the source scene tree."""
|
||||
if not self.controller or not self.model or not self.source_model_root:
|
||||
return
|
||||
|
||||
source_children = {}
|
||||
for child in self._iter_children(self.source_model_root):
|
||||
try:
|
||||
source_children[child.get_name()] = child
|
||||
except Exception:
|
||||
try:
|
||||
source_children[child.getName()] = child
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for key in self._get_top_level_group_keys():
|
||||
display_name = self.controller.display_names.get(key, key)
|
||||
source_child = source_children.get(display_name)
|
||||
if not source_child:
|
||||
grouped_entries = {}
|
||||
for gid, obj_np in self.controller.id_to_object_np.items():
|
||||
if not self._node_is_valid(obj_np):
|
||||
continue
|
||||
|
||||
group_ids = self.controller.name_to_ids.get(key, [])
|
||||
if not group_ids:
|
||||
owner_key = self.controller.id_to_name.get(gid)
|
||||
if not owner_key or owner_key == getattr(self.controller, "tree_root_key", None):
|
||||
continue
|
||||
|
||||
representative_id = None
|
||||
for gid in group_ids:
|
||||
obj_np = self.controller.id_to_object_np.get(gid)
|
||||
if obj_np and not obj_np.is_empty():
|
||||
representative_id = gid
|
||||
break
|
||||
if representative_id is None:
|
||||
source_node = self._resolve_source_node_by_tree_key(owner_key)
|
||||
if not self._node_is_valid(source_node):
|
||||
continue
|
||||
|
||||
try:
|
||||
current_mat = LMatrix4f(self.controller.id_to_object_np[representative_id].get_mat(self.model))
|
||||
current_net_mat = LMatrix4f(obj_np.get_mat(self.model))
|
||||
except Exception:
|
||||
try:
|
||||
current_mat = LMatrix4f(self.controller.id_to_object_np[representative_id].getMat(self.model))
|
||||
current_net_mat = LMatrix4f(obj_np.getMat(self.model))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if representative_id >= len(self.controller.global_transforms):
|
||||
existing_entry = grouped_entries.get(owner_key)
|
||||
if existing_entry is None:
|
||||
grouped_entries[owner_key] = {
|
||||
"source_node": source_node,
|
||||
"current_net_mat": current_net_mat,
|
||||
}
|
||||
|
||||
for owner_key in sorted(grouped_entries.keys(), key=lambda key: str(key).count("/")):
|
||||
entry = grouped_entries.get(owner_key) or {}
|
||||
source_node = entry.get("source_node")
|
||||
current_net_mat = entry.get("current_net_mat")
|
||||
if not self._node_is_valid(source_node) or current_net_mat is None:
|
||||
continue
|
||||
original_mat = LMatrix4f(self.controller.global_transforms[representative_id])
|
||||
inv_original = LMatrix4f(original_mat)
|
||||
|
||||
try:
|
||||
inv_original.invertInPlace()
|
||||
source_parent = source_node.get_parent()
|
||||
except Exception:
|
||||
try:
|
||||
inv_original.invert_in_place()
|
||||
source_parent = source_node.getParent()
|
||||
except Exception:
|
||||
continue
|
||||
source_parent = None
|
||||
|
||||
base_child_mat = self._source_child_base_mats.get(display_name)
|
||||
if base_child_mat is None:
|
||||
parent_net_mat = LMatrix4f.ident_mat()
|
||||
if self._node_is_valid(source_parent) and source_parent != self.source_model_root:
|
||||
try:
|
||||
base_child_mat = LMatrix4f(source_child.get_mat())
|
||||
parent_net_mat = LMatrix4f(source_parent.get_mat(self.source_model_root))
|
||||
except Exception:
|
||||
try:
|
||||
base_child_mat = LMatrix4f(source_child.getMat())
|
||||
parent_net_mat = LMatrix4f(source_parent.getMat(self.source_model_root))
|
||||
except Exception:
|
||||
continue
|
||||
parent_net_mat = LMatrix4f.ident_mat()
|
||||
|
||||
delta_mat = current_mat * inv_original
|
||||
inv_parent_mat = LMatrix4f(parent_net_mat)
|
||||
try:
|
||||
source_child.set_mat(delta_mat * base_child_mat)
|
||||
inv_parent_mat.invertInPlace()
|
||||
except Exception:
|
||||
try:
|
||||
source_child.setMat(delta_mat * base_child_mat)
|
||||
inv_parent_mat.invert_in_place()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
local_mat = current_net_mat * inv_parent_mat
|
||||
try:
|
||||
source_node.set_mat(local_mat)
|
||||
except Exception:
|
||||
try:
|
||||
source_node.setMat(local_mat)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
@ -1794,14 +1797,22 @@ class SSBOEditor:
|
||||
|
||||
|
||||
def on_mouse_click(self):
|
||||
io = imgui.get_io()
|
||||
if io.want_capture_mouse: return
|
||||
# Skip SSBO picking when user is interacting with the TransformGizmo,
|
||||
# otherwise pick_object would clear the selection and detach the gizmo
|
||||
# before the gizmo's own mouse handler fires.
|
||||
if self._transform_gizmo and self._transform_gizmo.is_hovering:
|
||||
return
|
||||
if self.base.mouseWatcherNode.has_mouse():
|
||||
try:
|
||||
win_width, win_height = self.base.win.getSize()
|
||||
mpos = self.base.mouseWatcherNode.get_mouse()
|
||||
window_x = (float(mpos.x) + 1.0) * 0.5 * float(win_width)
|
||||
window_y = (1.0 - float(mpos.y)) * 0.5 * float(win_height)
|
||||
process_imgui_click = getattr(self.base, "processImGuiMouseClick", None)
|
||||
if callable(process_imgui_click) and process_imgui_click(window_x, window_y):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
self._sync_pick_model_transform()
|
||||
self._refresh_ssbo_proxy_center()
|
||||
mpos = self.base.mouseWatcherNode.get_mouse()
|
||||
|
||||
@ -420,29 +420,22 @@ class AppActions:
|
||||
def _is_mouse_over_imgui(self):
|
||||
"""检测鼠标是否在ImGui窗口上"""
|
||||
try:
|
||||
# 检查是否有任何ImGui窗口想要捕获鼠标
|
||||
if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse:
|
||||
point = self._get_mouse_window_point()
|
||||
if point and self._is_point_in_known_imgui_rects(point):
|
||||
return True
|
||||
|
||||
# 检查鼠标是否在任何ImGui窗口内
|
||||
mouse_pos = self.mouseWatcherNode.getMouse()
|
||||
if not mouse_pos:
|
||||
return False
|
||||
|
||||
# 简单的边界检查(可以根据需要扩展)
|
||||
display_size = imgui.get_io().display_size
|
||||
mouse_x = mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2
|
||||
mouse_y = display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2)
|
||||
|
||||
# 检查是否在常见的ImGui界面区域内
|
||||
# 这里可以根据实际的ImGui窗口位置进行更精确的检测
|
||||
if mouse_x < 300 and mouse_y < 200: # 左上角区域(菜单栏)
|
||||
return True
|
||||
if mouse_x < 300 and mouse_y > display_size.y - 200: # 左下角区域(工具栏)
|
||||
return True
|
||||
if mouse_x > display_size.x - 300 and mouse_y < 200: # 右上角区域
|
||||
return True
|
||||
|
||||
|
||||
try:
|
||||
if imgui.is_any_item_active() or imgui.is_any_item_hovered():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if point and imgui.is_any_window_hovered() and self._is_point_in_known_imgui_rects(point):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"ImGui界面检测失败: {e}")
|
||||
@ -452,28 +445,64 @@ class AppActions:
|
||||
def processImGuiMouseClick(self, x, y):
|
||||
"""处理ImGui鼠标点击事件,返回是否消费了该事件"""
|
||||
try:
|
||||
# ImGui优先策略:如果ImGui想要捕获鼠标,则由ImGui处理
|
||||
if hasattr(imgui, 'get_io') and imgui.get_io().want_capture_mouse:
|
||||
point = (float(x), float(y))
|
||||
if self._is_point_in_known_imgui_rects(point):
|
||||
return True
|
||||
|
||||
# 检查是否有任何ImGui窗口悬停
|
||||
|
||||
try:
|
||||
if imgui.is_any_window_hovered():
|
||||
if imgui.is_any_item_active() or imgui.is_any_item_hovered():
|
||||
return True
|
||||
except AttributeError:
|
||||
# 如果方法不存在,跳过这个检查
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 检查鼠标是否在ImGui界面区域内
|
||||
if self._is_mouse_over_imgui():
|
||||
return True
|
||||
|
||||
# 如果以上条件都不满足,则让3D场景处理该事件
|
||||
|
||||
try:
|
||||
if imgui.is_any_window_hovered() and self._is_point_in_known_imgui_rects(point):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"ImGui鼠标点击处理失败: {e}")
|
||||
return False
|
||||
|
||||
def _get_mouse_window_point(self):
|
||||
try:
|
||||
if not self.mouseWatcherNode.hasMouse():
|
||||
return None
|
||||
mouse_pos = self.mouseWatcherNode.getMouse()
|
||||
if not mouse_pos:
|
||||
return None
|
||||
|
||||
display_size = imgui.get_io().display_size
|
||||
return (
|
||||
float(mouse_pos.get_x() * display_size.x / 2 + display_size.x / 2),
|
||||
float(display_size.y - (mouse_pos.get_y() * display_size.y / 2 + display_size.y / 2)),
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _point_in_rect(point, rect):
|
||||
if not point or not rect:
|
||||
return False
|
||||
x, y = point
|
||||
rx, ry, rw, rh = rect
|
||||
return rx <= x <= rx + rw and ry <= y <= ry + rh
|
||||
|
||||
def _is_point_in_known_imgui_rects(self, point):
|
||||
for rect_name in (
|
||||
"_resource_manager_window_rect",
|
||||
"_scene_tree_window_rect",
|
||||
"_property_panel_window_rect",
|
||||
"_script_panel_window_rect",
|
||||
"_console_window_rect",
|
||||
"_toolbar_window_rect",
|
||||
):
|
||||
if self._point_in_rect(point, getattr(self, rect_name, None)):
|
||||
return True
|
||||
return False
|
||||
|
||||
# ==================== 消息系统 ====================
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ class EditorPanelsLeftMixin:
|
||||
with self.app.style_manager.begin_styled_window("场景树", self.app.showSceneTree, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showSceneTree = False
|
||||
self.app._scene_tree_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showSceneTree = opened
|
||||
@ -430,6 +431,7 @@ class EditorPanelsLeftMixin:
|
||||
with self.app.style_manager.begin_styled_window("资源管理器", self.app.showResourceManager, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showResourceManager = False
|
||||
self.app._resource_manager_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showResourceManager = opened
|
||||
|
||||
@ -143,9 +143,18 @@ class EditorPanelsRightMixin(
|
||||
with self.app.style_manager.begin_styled_window("属性面板", self.app.showPropertyPanel, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showPropertyPanel = False
|
||||
self.app._property_panel_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showPropertyPanel = opened
|
||||
window_pos = imgui.get_window_pos()
|
||||
window_size = imgui.get_window_size()
|
||||
self.app._property_panel_window_rect = (
|
||||
float(window_pos.x),
|
||||
float(window_pos.y),
|
||||
float(window_size.x),
|
||||
float(window_size.y),
|
||||
)
|
||||
|
||||
# --- LUI Component Properties ---
|
||||
# 优先检查 LUI 组件选择
|
||||
|
||||
@ -8,7 +8,19 @@ class EditorPanelsRightMaterialMixin:
|
||||
self._material_edit_sessions = {}
|
||||
return self._material_edit_sessions
|
||||
|
||||
def _ensure_node_materials_are_editable(self, node):
|
||||
ensure_unique_fn = getattr(self.app, "_ensure_unique_materials_for_node", None)
|
||||
if callable(ensure_unique_fn):
|
||||
try:
|
||||
materials = ensure_unique_fn(node)
|
||||
if materials:
|
||||
return materials
|
||||
except Exception:
|
||||
pass
|
||||
return self.app._get_node_materials(node)
|
||||
|
||||
def _begin_material_edit_session(self, node, session_key):
|
||||
self._ensure_node_materials_are_editable(node)
|
||||
sessions = self._ensure_material_edit_sessions()
|
||||
sessions.setdefault(session_key, self.app._capture_node_material_snapshot(node))
|
||||
|
||||
@ -42,12 +54,14 @@ class EditorPanelsRightMaterialMixin:
|
||||
self._record_material_snapshot_command(node, before_snapshot, after_snapshot)
|
||||
|
||||
def _apply_material_change_with_history(self, node, apply_callback):
|
||||
self._ensure_node_materials_are_editable(node)
|
||||
before_snapshot = self.app._capture_node_material_snapshot(node)
|
||||
apply_callback()
|
||||
after_snapshot = self.app._capture_node_material_snapshot(node)
|
||||
self._record_material_snapshot_command(node, before_snapshot, after_snapshot)
|
||||
|
||||
def _select_texture_for_material_with_history(self, node, material, texture_type):
|
||||
self._ensure_node_materials_are_editable(node)
|
||||
before_snapshot = self.app._capture_node_material_snapshot(node)
|
||||
changed = self._select_texture_for_material(node, material, texture_type)
|
||||
if not changed:
|
||||
@ -58,7 +72,7 @@ class EditorPanelsRightMaterialMixin:
|
||||
|
||||
def _draw_appearance_properties(self, node):
|
||||
"""绘制材质属性(Unity风格主材质入口)。"""
|
||||
materials = self.app._get_node_materials(node)
|
||||
materials = self._ensure_node_materials_are_editable(node)
|
||||
if not materials:
|
||||
fallback_material = self.app._ensure_material_for_node(node)
|
||||
materials = [fallback_material] if fallback_material else []
|
||||
@ -168,7 +182,7 @@ class EditorPanelsRightMaterialMixin:
|
||||
|
||||
def _draw_material_properties(self, node):
|
||||
"""绘制材质属性"""
|
||||
materials = self.app._get_node_materials(node)
|
||||
materials = self._ensure_node_materials_are_editable(node)
|
||||
|
||||
if not materials:
|
||||
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "无材质")
|
||||
|
||||
@ -286,9 +286,18 @@ class EditorPanelsTopMixin:
|
||||
with self.app.style_manager.begin_styled_window("工具栏", self.app.showToolbar, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showToolbar = False
|
||||
self.app._toolbar_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showToolbar = opened
|
||||
window_pos = imgui.get_window_pos()
|
||||
window_size = imgui.get_window_size()
|
||||
self.app._toolbar_window_rect = (
|
||||
float(window_pos.x),
|
||||
float(window_pos.y),
|
||||
float(window_size.x),
|
||||
float(window_size.y),
|
||||
)
|
||||
style_manager = self.app.style_manager
|
||||
command_manager = getattr(self.app, "command_manager", None)
|
||||
can_undo = command_manager.can_undo() if command_manager else False
|
||||
|
||||
@ -1206,6 +1206,142 @@ class PropertyHelpers:
|
||||
unique_materials.append(material)
|
||||
return unique_materials
|
||||
|
||||
def _clone_material_for_node(self, material, node):
|
||||
"""Clone one material so edits stay scoped to the selected node only."""
|
||||
try:
|
||||
from panda3d.core import Material
|
||||
|
||||
cloned_material = Material(material)
|
||||
source_name = ""
|
||||
try:
|
||||
source_name = material.get_name() or ""
|
||||
except Exception:
|
||||
try:
|
||||
source_name = material.getName() or ""
|
||||
except Exception:
|
||||
source_name = ""
|
||||
|
||||
node_name = self._get_node_name(node, "node") if hasattr(self, "_get_node_name") else "node"
|
||||
clone_name = f"{source_name or 'material'}__editable__{node_name}"
|
||||
try:
|
||||
cloned_material.set_name(clone_name)
|
||||
except Exception:
|
||||
try:
|
||||
cloned_material.setName(clone_name)
|
||||
except Exception:
|
||||
pass
|
||||
return cloned_material
|
||||
except Exception:
|
||||
return material
|
||||
|
||||
def _ensure_unique_materials_for_node(self, node):
|
||||
"""Detach shared/inherited materials so editing one child does not affect siblings."""
|
||||
try:
|
||||
from panda3d.core import MaterialAttrib
|
||||
|
||||
if not node or node.isEmpty():
|
||||
return []
|
||||
|
||||
materials = self._get_node_materials(node)
|
||||
if not materials:
|
||||
return []
|
||||
|
||||
current_signature = tuple(
|
||||
self._get_material_identity_key(material)
|
||||
for material in materials
|
||||
if material is not None
|
||||
)
|
||||
|
||||
try:
|
||||
stored_signature = tuple(node.getPythonTag("_editable_material_signature"))
|
||||
except Exception:
|
||||
stored_signature = None
|
||||
|
||||
if stored_signature == current_signature:
|
||||
return materials
|
||||
|
||||
node_material_key = None
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
node_material_key = self._get_material_identity_key(node.getMaterial())
|
||||
except Exception:
|
||||
node_material_key = None
|
||||
|
||||
changed = False
|
||||
cloned_materials = []
|
||||
|
||||
for material in materials:
|
||||
if material is None:
|
||||
continue
|
||||
|
||||
cloned_material = self._clone_material_for_node(material, node)
|
||||
cloned_materials.append(cloned_material)
|
||||
source_key = self._get_material_identity_key(material)
|
||||
|
||||
if cloned_material is material:
|
||||
continue
|
||||
|
||||
try:
|
||||
if node_material_key is not None and source_key == node_material_key:
|
||||
node.setMaterial(cloned_material, 1)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
geom_paths = self._get_geom_paths_for_material(node, material)
|
||||
if not geom_paths and node_material_key is None:
|
||||
try:
|
||||
node.setMaterial(cloned_material, 1)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for geom_path in geom_paths:
|
||||
try:
|
||||
geom_path.setMaterial(cloned_material, 1)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(cloned_material)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
geom_node = geom_path.node()
|
||||
for geom_index in range(geom_node.getNumGeoms()):
|
||||
geom_state = geom_node.getGeomState(geom_index)
|
||||
geom_node.setGeomState(
|
||||
geom_index,
|
||||
geom_state.setAttrib(MaterialAttrib.make(cloned_material)),
|
||||
)
|
||||
changed = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if changed:
|
||||
self._invalidate_material_render_cache()
|
||||
|
||||
latest_materials = self._get_node_materials(node)
|
||||
if not latest_materials:
|
||||
latest_materials = cloned_materials
|
||||
|
||||
latest_signature = tuple(
|
||||
self._get_material_identity_key(material)
|
||||
for material in latest_materials
|
||||
if material is not None
|
||||
)
|
||||
try:
|
||||
node.setPythonTag("_editable_material_signature", latest_signature)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return latest_materials
|
||||
except Exception as e:
|
||||
print(f"隔离节点材质实例失败: {e}")
|
||||
return self._get_node_materials(node)
|
||||
|
||||
def _get_material_identity_key(self, material):
|
||||
try:
|
||||
return getattr(material, "this", None) or id(material)
|
||||
@ -1932,6 +2068,11 @@ class PropertyHelpers:
|
||||
def _reset_material(self, node):
|
||||
"""重置节点材质"""
|
||||
try:
|
||||
materials = self._ensure_unique_materials_for_node(node)
|
||||
if not materials:
|
||||
fallback_material = self._ensure_material_for_node(node)
|
||||
materials = [fallback_material] if fallback_material else []
|
||||
|
||||
# 先清理贴图与effect标签,避免后续再次设置贴图时被旧状态污染
|
||||
try:
|
||||
self._clear_all_textures(node)
|
||||
@ -1942,8 +2083,6 @@ class PropertyHelpers:
|
||||
node.clearTag("material_render_effect_signature")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
materials = list(node.find_all_materials())
|
||||
|
||||
for material in materials:
|
||||
# 重置为默认材质属性
|
||||
|
||||
@ -25,9 +25,18 @@ class ScriptPanels:
|
||||
with self.style_manager.begin_styled_window("控制台", self.app.showConsole, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showConsole = False
|
||||
self.app._console_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showConsole = opened
|
||||
window_pos = imgui.get_window_pos()
|
||||
window_size = imgui.get_window_size()
|
||||
self.app._console_window_rect = (
|
||||
float(window_pos.x),
|
||||
float(window_pos.y),
|
||||
float(window_size.x),
|
||||
float(window_size.y),
|
||||
)
|
||||
|
||||
imgui.text("控制台输出")
|
||||
imgui.separator()
|
||||
@ -111,9 +120,18 @@ class ScriptPanels:
|
||||
with self.style_manager.begin_styled_window("脚本管理", self.app.showScriptPanel, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showScriptPanel = False
|
||||
self.app._script_panel_window_rect = None
|
||||
return
|
||||
|
||||
self.app.showScriptPanel = opened
|
||||
window_pos = imgui.get_window_pos()
|
||||
window_size = imgui.get_window_size()
|
||||
self.app._script_panel_window_rect = (
|
||||
float(window_pos.x),
|
||||
float(window_pos.y),
|
||||
float(window_size.x),
|
||||
float(window_size.y),
|
||||
)
|
||||
|
||||
# 1. 脚本系统状态组
|
||||
self._draw_script_status_group()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user