撤销和重做优化
This commit is contained in:
parent
1cfbeda77b
commit
6e76b54ddb
@ -462,10 +462,57 @@ class TransformGizmo(DirectObject):
|
||||
Callback used by MoveGizmo / RotateGizmo / ScaleGizmo to report that a
|
||||
transform action (move/rotate/scale) has been committed.
|
||||
"""
|
||||
if self._record_action_with_command_manager(action):
|
||||
return
|
||||
|
||||
self._history.append(action)
|
||||
# New user action invalidates redo chain.
|
||||
self._redo_history.clear()
|
||||
|
||||
def _record_action_with_command_manager(self, action: Dict[str, Any]) -> bool:
|
||||
"""Prefer routing transform actions into the global command manager."""
|
||||
command_manager = getattr(self.world, "command_manager", None)
|
||||
if not command_manager:
|
||||
return False
|
||||
|
||||
try:
|
||||
from core.Command_System import MoveNodeCommand, RotateNodeCommand, ScaleNodeCommand
|
||||
|
||||
kind = action.get("kind")
|
||||
node: NodePath = action.get("node")
|
||||
if node is None or node.isEmpty():
|
||||
return False
|
||||
|
||||
command = None
|
||||
if kind == "move":
|
||||
command = MoveNodeCommand(
|
||||
node,
|
||||
action.get("old_pos"),
|
||||
action.get("new_pos"),
|
||||
reference_node=self.world.render,
|
||||
)
|
||||
elif kind == "rotate":
|
||||
command = RotateNodeCommand(
|
||||
node,
|
||||
action.get("old_hpr"),
|
||||
action.get("new_hpr"),
|
||||
reference_node=self.world.render,
|
||||
)
|
||||
elif kind == "scale":
|
||||
command = ScaleNodeCommand(
|
||||
node,
|
||||
action.get("old_scale"),
|
||||
action.get("new_scale"),
|
||||
)
|
||||
|
||||
if command is None:
|
||||
return False
|
||||
|
||||
command_manager.execute_command(command)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _sync_light_position_if_needed(self, node: Optional[NodePath]) -> None:
|
||||
"""When target node wraps an RP light, keep RP light position in sync."""
|
||||
try:
|
||||
|
||||
@ -1,10 +1,77 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import deque
|
||||
from typing import List
|
||||
from panda3d.core import NodePath, Point3
|
||||
|
||||
|
||||
class Command(ABC):
|
||||
from typing import List
|
||||
from panda3d.core import NodePath, Point3
|
||||
|
||||
|
||||
def _is_valid_node(node) -> bool:
|
||||
return bool(node) and hasattr(node, "isEmpty") and (not node.isEmpty())
|
||||
|
||||
|
||||
def _register_scene_node(world, node: NodePath):
|
||||
if not world or not _is_valid_node(node):
|
||||
return
|
||||
scene_manager = getattr(world, "scene_manager", None)
|
||||
if scene_manager and hasattr(scene_manager, "models") and node not in scene_manager.models:
|
||||
scene_manager.models.append(node)
|
||||
try:
|
||||
if hasattr(world, "updateSceneTree"):
|
||||
world.updateSceneTree()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _unregister_scene_node(world, node: NodePath):
|
||||
if not world or not node:
|
||||
return
|
||||
scene_manager = getattr(world, "scene_manager", None)
|
||||
if scene_manager and hasattr(scene_manager, "models"):
|
||||
try:
|
||||
while node in scene_manager.models:
|
||||
scene_manager.models.remove(node)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if hasattr(world, "updateSceneTree"):
|
||||
world.updateSceneTree()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _refresh_scene_tree(world):
|
||||
if not world:
|
||||
return
|
||||
try:
|
||||
if hasattr(world, "updateSceneTree"):
|
||||
world.updateSceneTree()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _apply_vec3_method(node, method_name: str, value, reference_node=None):
|
||||
method = getattr(node, method_name)
|
||||
if reference_node is not None:
|
||||
try:
|
||||
method(reference_node, value)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
if isinstance(value, (tuple, list)) and len(value) >= 3:
|
||||
method(reference_node, value[0], value[1], value[2])
|
||||
return
|
||||
else:
|
||||
try:
|
||||
method(value)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
if isinstance(value, (tuple, list)) and len(value) >= 3:
|
||||
method(value[0], value[1], value[2])
|
||||
return
|
||||
method(value)
|
||||
|
||||
|
||||
class Command(ABC):
|
||||
"""
|
||||
抽象命令类,所有具体命令都需要继承此类
|
||||
"""
|
||||
@ -44,18 +111,27 @@ class CommandManager:
|
||||
# 最大历史记录数
|
||||
self._max_history = max_history
|
||||
|
||||
def execute_command(self, command: Command):
|
||||
"""
|
||||
执行命令,并将其添加到撤销栈中
|
||||
"""
|
||||
try:
|
||||
command.execute()
|
||||
self._undo_stack.append(command)
|
||||
# 清空重做栈,因为执行新命令后就无法重做之前的命令了
|
||||
self._redo_stack.clear()
|
||||
except Exception as e:
|
||||
print(f"执行命令时出错: {e}")
|
||||
raise
|
||||
def execute_command(self, command: Command):
|
||||
"""
|
||||
执行命令,并将其添加到撤销栈中
|
||||
"""
|
||||
try:
|
||||
command.execute()
|
||||
self.record_command(command)
|
||||
except Exception as e:
|
||||
print(f"执行命令时出错: {e}")
|
||||
raise
|
||||
|
||||
def record_command(self, command: Command):
|
||||
"""记录一个已经执行完成的命令。"""
|
||||
self._undo_stack.append(command)
|
||||
self._redo_stack.clear()
|
||||
|
||||
def pop_last_command(self):
|
||||
"""弹出最后一个已执行命令,供复合操作合并历史使用。"""
|
||||
if not self._undo_stack:
|
||||
return None
|
||||
return self._undo_stack.pop()
|
||||
|
||||
def undo(self) -> bool:
|
||||
"""
|
||||
@ -128,33 +204,34 @@ class CommandManager:
|
||||
|
||||
|
||||
# 示例命令实现
|
||||
class MoveNodeCommand(Command):
|
||||
class MoveNodeCommand(Command):
|
||||
"""
|
||||
移动节点命令示例
|
||||
"""
|
||||
|
||||
def __init__(self, node: NodePath, old_pos, new_pos):
|
||||
self.node = node
|
||||
self.old_pos = old_pos
|
||||
self.new_pos = new_pos
|
||||
def __init__(self, node: NodePath, old_pos, new_pos, reference_node=None):
|
||||
self.node = node
|
||||
self.old_pos = old_pos
|
||||
self.new_pos = new_pos
|
||||
self.reference_node = reference_node
|
||||
|
||||
def execute(self):
|
||||
"""
|
||||
执行移动操作
|
||||
"""
|
||||
self.node.setPos(self.new_pos)
|
||||
def execute(self):
|
||||
"""
|
||||
执行移动操作
|
||||
"""
|
||||
_apply_vec3_method(self.node, "setPos", self.new_pos, self.reference_node)
|
||||
|
||||
def undo(self):
|
||||
"""
|
||||
撤销移动操作
|
||||
"""
|
||||
self.node.setPos(self.old_pos)
|
||||
_apply_vec3_method(self.node, "setPos", self.old_pos, self.reference_node)
|
||||
|
||||
def redo(self):
|
||||
"""
|
||||
重做移动操作
|
||||
"""
|
||||
self.node.setPos(self.new_pos)
|
||||
_apply_vec3_method(self.node, "setPos", self.new_pos, self.reference_node)
|
||||
|
||||
|
||||
class DeleteNodeCommand(Command):
|
||||
@ -336,36 +413,37 @@ class DeleteNodeCommand(Command):
|
||||
self.execute()
|
||||
|
||||
|
||||
class RotateNodeCommand(Command):
|
||||
class RotateNodeCommand(Command):
|
||||
"""
|
||||
旋转节点命令
|
||||
"""
|
||||
|
||||
def __init__(self, node: NodePath, old_hpr, new_hpr):
|
||||
self.node = node
|
||||
self.old_hpr = old_hpr
|
||||
self.new_hpr = new_hpr
|
||||
def __init__(self, node: NodePath, old_hpr, new_hpr, reference_node=None):
|
||||
self.node = node
|
||||
self.old_hpr = old_hpr
|
||||
self.new_hpr = new_hpr
|
||||
self.reference_node = reference_node
|
||||
|
||||
def execute(self):
|
||||
"""
|
||||
执行旋转操作
|
||||
"""
|
||||
self.node.setHpr(self.new_hpr)
|
||||
def execute(self):
|
||||
"""
|
||||
执行旋转操作
|
||||
"""
|
||||
_apply_vec3_method(self.node, "setHpr", self.new_hpr, self.reference_node)
|
||||
|
||||
def undo(self):
|
||||
"""
|
||||
撤销旋转操作
|
||||
"""
|
||||
self.node.setHpr(self.old_hpr)
|
||||
_apply_vec3_method(self.node, "setHpr", self.old_hpr, self.reference_node)
|
||||
|
||||
def redo(self):
|
||||
"""
|
||||
重做旋转操作
|
||||
"""
|
||||
self.node.setHpr(self.new_hpr)
|
||||
_apply_vec3_method(self.node, "setHpr", self.new_hpr, self.reference_node)
|
||||
|
||||
|
||||
class ScaleNodeCommand(Command):
|
||||
class ScaleNodeCommand(Command):
|
||||
"""
|
||||
缩放节点命令
|
||||
"""
|
||||
@ -375,56 +453,176 @@ class ScaleNodeCommand(Command):
|
||||
self.old_scale = old_scale
|
||||
self.new_scale = new_scale
|
||||
|
||||
def execute(self):
|
||||
"""
|
||||
执行缩放操作
|
||||
"""
|
||||
self.node.setScale(self.new_scale)
|
||||
def execute(self):
|
||||
"""
|
||||
执行缩放操作
|
||||
"""
|
||||
_apply_vec3_method(self.node, "setScale", self.new_scale)
|
||||
|
||||
def undo(self):
|
||||
"""
|
||||
撤销缩放操作
|
||||
"""
|
||||
self.node.setScale(self.old_scale)
|
||||
_apply_vec3_method(self.node, "setScale", self.old_scale)
|
||||
|
||||
def redo(self):
|
||||
"""
|
||||
重做缩放操作
|
||||
"""
|
||||
self.node.setScale(self.new_scale)
|
||||
def redo(self):
|
||||
"""
|
||||
重做缩放操作
|
||||
"""
|
||||
_apply_vec3_method(self.node, "setScale", self.new_scale)
|
||||
|
||||
|
||||
class RenameNodeCommand(Command):
|
||||
"""Rename a node and refresh scene tree bindings."""
|
||||
|
||||
def __init__(self, node: NodePath, old_name: str, new_name: str, world=None):
|
||||
self.node = node
|
||||
self.old_name = old_name
|
||||
self.new_name = new_name
|
||||
self.world = world
|
||||
|
||||
def execute(self):
|
||||
if self.node and not self.node.isEmpty():
|
||||
self.node.setName(self.new_name)
|
||||
_refresh_scene_tree(self.world)
|
||||
|
||||
def undo(self):
|
||||
if self.node and not self.node.isEmpty():
|
||||
self.node.setName(self.old_name)
|
||||
_refresh_scene_tree(self.world)
|
||||
|
||||
def redo(self):
|
||||
self.execute()
|
||||
|
||||
|
||||
class VisibilityNodeCommand(Command):
|
||||
"""Toggle editor visibility state for a node."""
|
||||
|
||||
def __init__(self, node: NodePath, old_visible: bool, new_visible: bool):
|
||||
self.node = node
|
||||
self.old_visible = bool(old_visible)
|
||||
self.new_visible = bool(new_visible)
|
||||
|
||||
def _apply(self, visible: bool):
|
||||
if not self.node or self.node.isEmpty():
|
||||
return
|
||||
self.node.setPythonTag("user_visible", bool(visible))
|
||||
if visible:
|
||||
self.node.show()
|
||||
else:
|
||||
self.node.hide()
|
||||
|
||||
def execute(self):
|
||||
self._apply(self.new_visible)
|
||||
|
||||
def undo(self):
|
||||
self._apply(self.old_visible)
|
||||
|
||||
def redo(self):
|
||||
self.execute()
|
||||
|
||||
|
||||
class MaterialStateCommand(Command):
|
||||
"""Replay a captured material snapshot for undo/redo."""
|
||||
|
||||
def __init__(self, apply_state_callback, before_state, after_state):
|
||||
self.apply_state_callback = apply_state_callback
|
||||
self.before_state = before_state
|
||||
self.after_state = after_state
|
||||
|
||||
def execute(self):
|
||||
if self.apply_state_callback:
|
||||
self.apply_state_callback(self.after_state)
|
||||
|
||||
def undo(self):
|
||||
if self.apply_state_callback:
|
||||
self.apply_state_callback(self.before_state)
|
||||
|
||||
def redo(self):
|
||||
self.execute()
|
||||
|
||||
|
||||
class SnapshotStateCommand(MaterialStateCommand):
|
||||
"""Generic callback-based snapshot command."""
|
||||
|
||||
|
||||
class CreateNodeCommand(Command):
|
||||
"""
|
||||
创建节点命令
|
||||
"""
|
||||
|
||||
|
||||
class CreateNodeCommand(Command):
|
||||
"""
|
||||
创建节点命令
|
||||
"""
|
||||
|
||||
def __init__(self, node_creator_func,parent_node, *args, **kwargs):
|
||||
self.node_creator_func = node_creator_func
|
||||
self.parent_node = parent_node
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.created_node = None
|
||||
def __init__(self, node_creator_func, parent_node, *args, world=None, **kwargs):
|
||||
self.node_creator_func = node_creator_func
|
||||
self.parent_node = parent_node
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.world = world
|
||||
self.created_node = None
|
||||
|
||||
def execute(self):
|
||||
"""
|
||||
执行创建节点操作
|
||||
"""
|
||||
self.created_node = self.node_creator_func(self.parent_node,*self.args, **self.kwargs)
|
||||
return self.created_node
|
||||
if self.created_node and not self.created_node.isEmpty():
|
||||
target_parent = self.parent_node
|
||||
if (not target_parent or target_parent.isEmpty()) and self.world:
|
||||
target_parent = getattr(self.world, "render", None)
|
||||
if target_parent and not target_parent.isEmpty():
|
||||
self.created_node.wrtReparentTo(target_parent)
|
||||
_register_scene_node(self.world, self.created_node)
|
||||
return self.created_node
|
||||
|
||||
self.created_node = self.node_creator_func(self.parent_node, *self.args, **self.kwargs)
|
||||
_register_scene_node(self.world, self.created_node)
|
||||
return self.created_node
|
||||
|
||||
def undo(self):
|
||||
"""
|
||||
撤销创建节点操作
|
||||
"""
|
||||
if self.created_node:
|
||||
self.created_node.detachNode()
|
||||
"""
|
||||
撤销创建节点操作
|
||||
"""
|
||||
if self.created_node:
|
||||
_unregister_scene_node(self.world, self.created_node)
|
||||
self.created_node.detachNode()
|
||||
|
||||
def redo(self):
|
||||
"""
|
||||
重做创建节点操作
|
||||
"""
|
||||
self.execute()
|
||||
"""
|
||||
重做创建节点操作
|
||||
"""
|
||||
if self.created_node and not self.created_node.isEmpty():
|
||||
target_parent = self.parent_node
|
||||
if (not target_parent or target_parent.isEmpty()) and self.world:
|
||||
target_parent = getattr(self.world, "render", None)
|
||||
if target_parent and not target_parent.isEmpty():
|
||||
self.created_node.wrtReparentTo(target_parent)
|
||||
_register_scene_node(self.world, self.created_node)
|
||||
return
|
||||
self.execute()
|
||||
|
||||
|
||||
class AttachNodeCommand(Command):
|
||||
"""Attach an existing detached node into a parent and make it undoable."""
|
||||
|
||||
def __init__(self, node: NodePath, parent_node: NodePath, world=None):
|
||||
self.node = node
|
||||
self.parent_node = parent_node
|
||||
self.world = world
|
||||
|
||||
def execute(self):
|
||||
target_parent = self.parent_node
|
||||
if (not target_parent or target_parent.isEmpty()) and self.world:
|
||||
target_parent = getattr(self.world, "render", None)
|
||||
if self.node and not self.node.isEmpty() and target_parent and not target_parent.isEmpty():
|
||||
self.node.wrtReparentTo(target_parent)
|
||||
_register_scene_node(self.world, self.node)
|
||||
return self.node
|
||||
|
||||
def undo(self):
|
||||
if self.node and not self.node.isEmpty():
|
||||
_unregister_scene_node(self.world, self.node)
|
||||
self.node.detachNode()
|
||||
|
||||
def redo(self):
|
||||
self.execute()
|
||||
|
||||
|
||||
class ReparentNodeCommand(Command):
|
||||
|
||||
24
imgui.ini
24
imgui.ini
@ -25,25 +25,25 @@ Collapsed=0
|
||||
|
||||
[Window][工具栏]
|
||||
Pos=354,20
|
||||
Size=1334,32
|
||||
Size=1846,32
|
||||
Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][场景树]
|
||||
Pos=0,20
|
||||
Size=352,748
|
||||
Size=352,995
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
|
||||
[Window][属性面板]
|
||||
Pos=1690,20
|
||||
Size=358,748
|
||||
Pos=2202,20
|
||||
Size=358,995
|
||||
Collapsed=0
|
||||
DockId=0x00000002,0
|
||||
|
||||
[Window][控制台]
|
||||
Pos=1690,20
|
||||
Size=358,748
|
||||
Pos=2202,20
|
||||
Size=358,995
|
||||
Collapsed=0
|
||||
DockId=0x00000002,1
|
||||
|
||||
@ -60,7 +60,7 @@ Collapsed=0
|
||||
|
||||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,20
|
||||
Size=2048,1084
|
||||
Size=2560,1331
|
||||
Collapsed=0
|
||||
|
||||
[Window][测试窗口1]
|
||||
@ -79,7 +79,7 @@ Size=93,65
|
||||
Collapsed=0
|
||||
|
||||
[Window][新建项目]
|
||||
Pos=824,401
|
||||
Pos=824,402
|
||||
Size=400,300
|
||||
Collapsed=0
|
||||
|
||||
@ -99,8 +99,8 @@ Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][资源管理器]
|
||||
Pos=0,770
|
||||
Size=2048,334
|
||||
Pos=0,1017
|
||||
Size=2560,334
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
|
||||
@ -207,13 +207,13 @@ Collapsed=0
|
||||
DockId=0x00000002,2
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=Y
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2560,1331 Split=Y
|
||||
DockNode ID=0x00000005 Parent=0x08BD597D SizeRef=2560,995 Split=X
|
||||
DockNode ID=0x00000007 Parent=0x00000005 SizeRef=352,1084 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000005 SizeRef=2206,1084 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1846,989 Split=Y
|
||||
DockNode ID=0x0000000D Parent=0x00000001 SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
|
||||
DockNode ID=0x0000000E Parent=0x00000001 SizeRef=1318,714 CentralNode=1
|
||||
DockNode ID=0x00000002 Parent=0x00000008 SizeRef=358,989 Selected=0x5428E753
|
||||
DockNode ID=0x00000002 Parent=0x00000008 SizeRef=358,989 Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000006 Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3
|
||||
|
||||
|
||||
66
main.py
66
main.py
@ -135,7 +135,7 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self.win.request_properties(props)
|
||||
self._last_forced_window_sync_size = None
|
||||
self._remaining_window_sync_attempts = 6
|
||||
self.taskMgr.doMethodLater(0.05, self._sync_window_metrics_task, "initial-window-metrics-sync")
|
||||
self.taskMgr.doMethodLater(0.05, self._sync_window_metrics_task, "window-metrics-sync")
|
||||
print(f"✓ 窗口初始化完成: {self.win.getXSize()} x {self.win.getYSize()}")
|
||||
|
||||
# Legacy compatibility fields used by scene/project modules.
|
||||
@ -389,6 +389,9 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self.show_new_project_dialog = False
|
||||
self.show_open_project_dialog = False
|
||||
self.show_save_as_dialog = False
|
||||
self.show_about_dialog = False
|
||||
self.save_as_project_name = ""
|
||||
self.save_as_project_path = ""
|
||||
|
||||
# 路径选择对话框状态
|
||||
self.show_path_browser = False
|
||||
@ -630,11 +633,61 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
if not target_window:
|
||||
return
|
||||
self.windowEvent(target_window)
|
||||
self._sync_window_dependent_state(target_window)
|
||||
if not target_window.getProperties().getOpen():
|
||||
self.userExit()
|
||||
except Exception as e:
|
||||
print(f"处理窗口事件失败: {e}")
|
||||
|
||||
def _sync_window_dependent_state(self, window):
|
||||
"""Keep camera and picking lenses aligned with the current client size."""
|
||||
try:
|
||||
width = int(window.getXSize())
|
||||
height = int(window.getYSize())
|
||||
if width <= 0 or height <= 0:
|
||||
return
|
||||
|
||||
render_pipeline = getattr(self, "render_pipeline", None)
|
||||
if render_pipeline:
|
||||
try:
|
||||
pipeline_window_dims = getattr(render_pipeline, "_last_window_dims", None)
|
||||
pipeline_needs_resize = (
|
||||
pipeline_window_dims is None
|
||||
or int(pipeline_window_dims.x) != width
|
||||
or int(pipeline_window_dims.y) != height
|
||||
)
|
||||
if pipeline_needs_resize and hasattr(render_pipeline, "_handle_window_event"):
|
||||
render_pipeline._handle_window_event(window)
|
||||
except Exception as e:
|
||||
print(f"同步 RenderPipeline 窗口尺寸失败: {e}")
|
||||
|
||||
aspect_ratio = float(width) / float(height)
|
||||
|
||||
lens = getattr(self, "camLens", None)
|
||||
if lens:
|
||||
try:
|
||||
lens.setAspectRatio(aspect_ratio)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
cam = getattr(self, "cam", None)
|
||||
if cam:
|
||||
try:
|
||||
cam_lens = cam.node().getLens()
|
||||
if cam_lens:
|
||||
cam_lens.setAspectRatio(aspect_ratio)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
if ssbo_editor and getattr(ssbo_editor, "pick_lens", None):
|
||||
try:
|
||||
ssbo_editor.pick_lens.setAspectRatio(aspect_ratio)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"同步窗口依赖状态失败: {e}")
|
||||
|
||||
def _sync_window_metrics_task(self, task):
|
||||
"""Force early window-size synchronization so RP/ImGui match the real client area."""
|
||||
if self._shutdown_in_progress:
|
||||
@ -652,14 +705,17 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self._last_forced_window_sync_size = current_size
|
||||
messenger.send("window-event", [self.win])
|
||||
print(f"✓ 同步窗口分辨率: {width} x {height}")
|
||||
else:
|
||||
self._sync_window_dependent_state(self.win)
|
||||
except Exception as e:
|
||||
print(f"同步窗口分辨率失败: {e}")
|
||||
|
||||
self._remaining_window_sync_attempts -= 1
|
||||
if self._remaining_window_sync_attempts > 0:
|
||||
self._remaining_window_sync_attempts -= 1
|
||||
task.delayTime = 0.15
|
||||
return task.again
|
||||
return task.done
|
||||
else:
|
||||
task.delayTime = 0.05
|
||||
return task.again
|
||||
|
||||
def userExit(self):
|
||||
"""统一退出流程,避免关闭窗口后进程残留。"""
|
||||
@ -952,6 +1008,7 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
# 绘制对话框
|
||||
self._draw_new_project_dialog()
|
||||
self._draw_open_project_dialog()
|
||||
self._draw_save_as_project_dialog()
|
||||
self._draw_path_browser()
|
||||
self._draw_import_dialog()
|
||||
|
||||
@ -970,6 +1027,7 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
# 绘制纹理选择对话框
|
||||
self._draw_texture_file_dialog()
|
||||
self._draw_script_browser()
|
||||
self._draw_about_dialog()
|
||||
|
||||
# 绘制右键菜单
|
||||
self._draw_context_menus()
|
||||
|
||||
@ -1248,6 +1248,11 @@ class SSBOEditor:
|
||||
not self.controller or not self.model):
|
||||
return False
|
||||
self._sync_pick_model_transform()
|
||||
|
||||
try:
|
||||
self.pick_lens.setAspectRatio(self.base.camLens.getAspectRatio())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.pick_lens.set_fov(0.1)
|
||||
self.pick_lens.set_film_offset(0, 0)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -39,6 +39,9 @@ class LUIManagerInteractionMixin:
|
||||
if canvas_data.get('fixed', False):
|
||||
return
|
||||
|
||||
if hasattr(self, "_begin_lui_structure_snapshot_session"):
|
||||
self._begin_lui_structure_snapshot_session("canvas_drag")
|
||||
|
||||
if canvas_data.get('fill_mode', False):
|
||||
canvas_data['fill_mode'] = False
|
||||
print("✓ 自动禁用填充模式以允许自由拖动")
|
||||
@ -357,6 +360,8 @@ class LUIManagerInteractionMixin:
|
||||
return True
|
||||
|
||||
if not self._is_left_mouse_down():
|
||||
if hasattr(self, "_finish_lui_structure_snapshot_session"):
|
||||
self._finish_lui_structure_snapshot_session("canvas_drag")
|
||||
self.dragging_canvas = None
|
||||
return True
|
||||
|
||||
@ -395,6 +400,8 @@ class LUIManagerInteractionMixin:
|
||||
|
||||
# Start drag now
|
||||
comp_data = self.pending_drag_comp
|
||||
if hasattr(self, "_begin_lui_structure_snapshot_session"):
|
||||
self._begin_lui_structure_snapshot_session("lui_drag")
|
||||
self.dragging_comp = comp_data
|
||||
self.dragging_index = self.pending_drag_index
|
||||
self._original_parent_obj = self.pending_drag_start_parent
|
||||
@ -510,6 +517,8 @@ class LUIManagerInteractionMixin:
|
||||
comp_obj.top = comp_data.get('top', 0)
|
||||
print(f"✓ 组件归位到逻辑父级")
|
||||
|
||||
if hasattr(self, "_finish_lui_structure_snapshot_session"):
|
||||
self._finish_lui_structure_snapshot_session("lui_drag")
|
||||
self.dragging_comp = None
|
||||
return True
|
||||
return False
|
||||
@ -795,6 +804,8 @@ class LUIManagerInteractionMixin:
|
||||
'width': comp_data.get('width', 100),
|
||||
'height': comp_data.get('height', 30),
|
||||
}
|
||||
if hasattr(self, "_begin_lui_structure_snapshot_session"):
|
||||
self._begin_lui_structure_snapshot_session("lui_resize")
|
||||
|
||||
def _update_resize_handles(self, task):
|
||||
# Restore selection if it was cleared immediately
|
||||
@ -1278,6 +1289,8 @@ class LUIManagerInteractionMixin:
|
||||
if anchor_pos:
|
||||
self._update_component_anchor_position(self.selected_index, anchor_pos)
|
||||
|
||||
if hasattr(self, "_finish_lui_structure_snapshot_session"):
|
||||
self._finish_lui_structure_snapshot_session("lui_resize")
|
||||
self.resizing_handle = None
|
||||
return True
|
||||
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import os
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import webbrowser
|
||||
|
||||
from imgui_bundle import imgui
|
||||
from direct.showbase.ShowBaseGlobal import globalClock
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
@ -256,9 +260,55 @@ class AppActions:
|
||||
|
||||
def _on_save_as_project(self):
|
||||
"""处理另存为项目菜单项"""
|
||||
self.add_info_message("另存为项目(功能待实现)")
|
||||
# TODO: 实现另存为对话框
|
||||
# self.show_save_as_dialog = True
|
||||
current_project_path = getattr(getattr(self, "project_manager", None), "current_project_path", None)
|
||||
if current_project_path:
|
||||
self.save_as_project_name = f"{os.path.basename(current_project_path)}_副本"
|
||||
self.save_as_project_path = os.path.dirname(current_project_path)
|
||||
else:
|
||||
self.save_as_project_name = "新项目"
|
||||
self.save_as_project_path = os.getcwd()
|
||||
self.add_info_message("打开另存为项目对话框")
|
||||
self.show_save_as_dialog = True
|
||||
|
||||
|
||||
def _show_about_dialog(self):
|
||||
"""显示关于对话框。"""
|
||||
self.show_about_dialog = True
|
||||
|
||||
|
||||
def _get_documentation_target(self):
|
||||
"""返回优先级最高的本地帮助文档。"""
|
||||
candidate_names = [
|
||||
"PROJECT_MODULE_INDEX.md",
|
||||
"IMGUI_MODULE_ANALYSIS.md",
|
||||
"PROJECT_FULL_CATALOG.md",
|
||||
"PROJECT_OPTIMIZATION_ANALYSIS.md",
|
||||
"AGENTS.md",
|
||||
]
|
||||
for candidate_name in candidate_names:
|
||||
candidate_path = os.path.join(os.getcwd(), candidate_name)
|
||||
if os.path.isfile(candidate_path):
|
||||
return candidate_path
|
||||
return None
|
||||
|
||||
|
||||
def _open_documentation(self):
|
||||
"""打开本地项目文档。"""
|
||||
doc_path = self._get_documentation_target()
|
||||
if not doc_path:
|
||||
self.add_error_message("未找到可用的项目文档")
|
||||
return False
|
||||
|
||||
try:
|
||||
if os.name == "nt":
|
||||
os.startfile(doc_path)
|
||||
else:
|
||||
webbrowser.open(f"file://{doc_path}")
|
||||
self.add_success_message(f"已打开文档: {os.path.basename(doc_path)}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.add_error_message(f"打开文档失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _on_exit(self):
|
||||
@ -623,6 +673,7 @@ class AppActions:
|
||||
|
||||
# 反序列化并添加节点
|
||||
created_any = False
|
||||
last_created_node = None
|
||||
source_nodes = getattr(self, "clipboard_source_nodes", []) or []
|
||||
for i, node_data in enumerate(self.clipboard):
|
||||
def _paste_create_node(parent):
|
||||
@ -659,10 +710,37 @@ class AppActions:
|
||||
return created
|
||||
|
||||
new_node = None
|
||||
if hasattr(self, "command_manager") and self.command_manager:
|
||||
if self.clipboard_mode == "cut" and i < len(source_nodes):
|
||||
source_node = source_nodes[i]
|
||||
if source_node and not source_node.isEmpty():
|
||||
try:
|
||||
from core.Command_System import AttachNodeCommand, CompositeCommand, DeleteNodeCommand
|
||||
|
||||
attach_cmd = AttachNodeCommand(source_node, parent_node, self)
|
||||
if hasattr(self, "command_manager") and self.command_manager:
|
||||
last_cmd = None
|
||||
if hasattr(self.command_manager, "pop_last_command"):
|
||||
last_cmd = self.command_manager.pop_last_command()
|
||||
|
||||
if isinstance(last_cmd, DeleteNodeCommand) and getattr(last_cmd, "node", None) == source_node:
|
||||
attach_cmd.execute()
|
||||
self.command_manager.record_command(CompositeCommand([last_cmd, attach_cmd]))
|
||||
new_node = source_node
|
||||
else:
|
||||
if last_cmd is not None:
|
||||
self.command_manager.record_command(last_cmd)
|
||||
self.command_manager.execute_command(attach_cmd)
|
||||
new_node = source_node
|
||||
else:
|
||||
attach_cmd.execute()
|
||||
new_node = source_node
|
||||
except Exception as e:
|
||||
print(f"[Paste] attach existing cut node failed, fallback to recreate: {e}")
|
||||
|
||||
if new_node is None and hasattr(self, "command_manager") and self.command_manager:
|
||||
try:
|
||||
from core.Command_System import CreateNodeCommand
|
||||
create_cmd = CreateNodeCommand(_paste_create_node, parent_node)
|
||||
create_cmd = CreateNodeCommand(_paste_create_node, parent_node, world=self)
|
||||
self.command_manager.execute_command(create_cmd)
|
||||
new_node = create_cmd.created_node
|
||||
except Exception as e:
|
||||
@ -673,6 +751,7 @@ class AppActions:
|
||||
|
||||
if new_node:
|
||||
created_any = True
|
||||
last_created_node = new_node
|
||||
# Ensure pasted model can be picked by legacy collision fallback.
|
||||
try:
|
||||
if hasattr(self, "scene_manager") and self.scene_manager:
|
||||
@ -689,6 +768,15 @@ class AppActions:
|
||||
else:
|
||||
self.add_error_message("节点反序列化失败")
|
||||
|
||||
if created_any and last_created_node and hasattr(self, "selection") and self.selection:
|
||||
try:
|
||||
self.selection.updateSelection(last_created_node)
|
||||
ssbo_editor = getattr(self, "ssbo_editor", None)
|
||||
if ssbo_editor and hasattr(ssbo_editor, "sync_scene_selection"):
|
||||
ssbo_editor.sync_scene_selection(last_created_node)
|
||||
except Exception as e:
|
||||
print(f"[Paste] select pasted node failed: {e}")
|
||||
|
||||
# 如果是剪切模式且粘贴成功,清空剪切板
|
||||
if created_any and self.clipboard_mode == "cut":
|
||||
self.clipboard = []
|
||||
@ -861,7 +949,6 @@ class AppActions:
|
||||
"""打开项目的具体实现(不依赖Qt)"""
|
||||
import json
|
||||
import datetime
|
||||
import os
|
||||
|
||||
try:
|
||||
# 检查项目管理器是否已初始化
|
||||
@ -940,10 +1027,6 @@ class AppActions:
|
||||
|
||||
def _create_new_project_impl(self, name, path):
|
||||
"""创建新项目的具体实现(不依赖Qt)"""
|
||||
import json
|
||||
import datetime
|
||||
import os
|
||||
|
||||
full_project_path = os.path.normpath(os.path.join(path, name))
|
||||
print(f"创建项目路径: {full_project_path}")
|
||||
|
||||
@ -986,6 +1069,84 @@ class AppActions:
|
||||
except Exception as e:
|
||||
print(f"创建项目失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _save_project_as_impl(self, name, path):
|
||||
"""将当前项目保存到新的项目目录。"""
|
||||
if not hasattr(self, "project_manager") or not self.project_manager:
|
||||
self.add_error_message("项目管理器未初始化")
|
||||
return False
|
||||
|
||||
if not name or not path:
|
||||
self.add_warning_message("项目名称和保存位置不能为空")
|
||||
return False
|
||||
|
||||
full_project_path = os.path.normpath(os.path.join(path, name))
|
||||
current_project_path = (
|
||||
os.path.normpath(self.project_manager.current_project_path)
|
||||
if self.project_manager.current_project_path
|
||||
else None
|
||||
)
|
||||
|
||||
if current_project_path and os.path.normcase(full_project_path) == os.path.normcase(current_project_path):
|
||||
self.add_warning_message("目标路径与当前项目相同,已执行普通保存")
|
||||
return self._save_project_impl()
|
||||
|
||||
try:
|
||||
if os.path.exists(full_project_path):
|
||||
if not os.path.isdir(full_project_path):
|
||||
self.add_error_message("目标路径已存在且不是文件夹")
|
||||
return False
|
||||
if os.listdir(full_project_path):
|
||||
self.add_error_message("目标目录已存在且非空,请选择空目录或新的项目名称")
|
||||
return False
|
||||
else:
|
||||
os.makedirs(full_project_path)
|
||||
|
||||
os.makedirs(os.path.join(full_project_path, "models"), exist_ok=True)
|
||||
os.makedirs(os.path.join(full_project_path, "textures"), exist_ok=True)
|
||||
os.makedirs(os.path.join(full_project_path, "scenes"), exist_ok=True)
|
||||
|
||||
previous_project_path = self.project_manager.current_project_path
|
||||
previous_project_config = dict(self.project_manager.project_config or {})
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
project_config = dict(previous_project_config)
|
||||
project_config.update({
|
||||
"name": name,
|
||||
"path": full_project_path,
|
||||
"last_modified": timestamp,
|
||||
"scene_file": "scenes/scene.bam",
|
||||
})
|
||||
if "created_at" in project_config:
|
||||
project_config["created_at"] = project_config.get("created_at") or timestamp
|
||||
elif "created" in project_config:
|
||||
project_config["created"] = project_config.get("created") or timestamp
|
||||
else:
|
||||
project_config["created_at"] = timestamp
|
||||
if "version" not in project_config:
|
||||
project_config["version"] = "1.0"
|
||||
|
||||
config_file = os.path.join(full_project_path, "project.json")
|
||||
with open(config_file, "w", encoding="utf-8") as f:
|
||||
json.dump(project_config, f, ensure_ascii=False, indent=4)
|
||||
|
||||
self.project_manager.current_project_path = full_project_path
|
||||
self.project_manager.project_config = project_config
|
||||
|
||||
if not self.project_manager.saveProject():
|
||||
self.project_manager.current_project_path = previous_project_path
|
||||
self.project_manager.project_config = previous_project_config
|
||||
self._update_window_title(os.path.basename(previous_project_path) if previous_project_path else "未命名项目")
|
||||
self.add_error_message("项目另存为失败")
|
||||
return False
|
||||
|
||||
self._update_window_title(name)
|
||||
self.add_success_message(f"项目另存为成功: {name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.add_error_message(f"项目另存为失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _update_window_title(self, project_name):
|
||||
|
||||
@ -13,70 +13,82 @@ class CreateActions:
|
||||
else:
|
||||
setattr(self.app, name, value)
|
||||
|
||||
def _execute_create_command(self, creator, success_message, failure_message, error_prefix):
|
||||
try:
|
||||
result = None
|
||||
if hasattr(self, "command_manager") and self.command_manager:
|
||||
from core.Command_System import CreateNodeCommand
|
||||
|
||||
command = CreateNodeCommand(
|
||||
lambda _parent: creator(),
|
||||
getattr(self, "render", None),
|
||||
world=self,
|
||||
)
|
||||
self.command_manager.execute_command(command)
|
||||
result = command.created_node
|
||||
else:
|
||||
result = creator()
|
||||
|
||||
if result:
|
||||
if hasattr(self, "selection") and self.selection and hasattr(self.selection, "updateSelection"):
|
||||
self.selection.updateSelection(result)
|
||||
self.add_success_message(success_message)
|
||||
else:
|
||||
self.add_error_message(failure_message)
|
||||
return result
|
||||
except Exception as e:
|
||||
self.add_error_message(f"{error_prefix}: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def _on_create_empty_object(self):
|
||||
"""创建空对象"""
|
||||
try:
|
||||
result = self.createEmptyObject()
|
||||
if result:
|
||||
self.add_success_message("空对象创建成功")
|
||||
else:
|
||||
self.add_error_message("空对象创建失败")
|
||||
return result
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建空对象失败: {str(e)}")
|
||||
return self._execute_create_command(
|
||||
self.createEmptyObject,
|
||||
"空对象创建成功",
|
||||
"空对象创建失败",
|
||||
"创建空对象失败",
|
||||
)
|
||||
|
||||
|
||||
def _on_create_cube(self):
|
||||
"""创建立方体"""
|
||||
try:
|
||||
result = self.createCube()
|
||||
if result:
|
||||
self.add_success_message("立方体创建成功")
|
||||
else:
|
||||
self.add_error_message("立方体创建失败")
|
||||
return result
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建立方体失败: {str(e)}")
|
||||
return self._execute_create_command(
|
||||
self.createCube,
|
||||
"立方体创建成功",
|
||||
"立方体创建失败",
|
||||
"创建立方体失败",
|
||||
)
|
||||
|
||||
|
||||
def _on_create_sphere(self):
|
||||
"""创建球体"""
|
||||
try:
|
||||
result = self.createSphere()
|
||||
if result:
|
||||
self.add_success_message("球体创建成功")
|
||||
else:
|
||||
self.add_error_message("球体创建失败")
|
||||
return result
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建球体失败: {str(e)}")
|
||||
return self._execute_create_command(
|
||||
self.createSphere,
|
||||
"球体创建成功",
|
||||
"球体创建失败",
|
||||
"创建球体失败",
|
||||
)
|
||||
|
||||
|
||||
def _on_create_cylinder(self):
|
||||
"""创建圆柱体"""
|
||||
try:
|
||||
result = self.createCylinder()
|
||||
if result:
|
||||
self.add_success_message("圆柱体创建成功")
|
||||
else:
|
||||
self.add_error_message("圆柱体创建失败")
|
||||
return result
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建圆柱体失败: {str(e)}")
|
||||
return self._execute_create_command(
|
||||
self.createCylinder,
|
||||
"圆柱体创建成功",
|
||||
"圆柱体创建失败",
|
||||
"创建圆柱体失败",
|
||||
)
|
||||
|
||||
|
||||
def _on_create_plane(self):
|
||||
"""创建平面"""
|
||||
try:
|
||||
result = self.createPlane()
|
||||
if result:
|
||||
self.add_success_message("平面创建成功")
|
||||
else:
|
||||
self.add_error_message("平面创建失败")
|
||||
return result
|
||||
except Exception as e:
|
||||
self.add_error_message(f"创建平面失败: {str(e)}")
|
||||
return self._execute_create_command(
|
||||
self.createPlane,
|
||||
"平面创建成功",
|
||||
"平面创建失败",
|
||||
"创建平面失败",
|
||||
)
|
||||
|
||||
def _on_create_spot_light(self):
|
||||
"""创建聚光灯"""
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from imgui_bundle import imgui, imgui_ctx
|
||||
|
||||
|
||||
@ -125,6 +127,67 @@ class DialogPanels:
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_open_project_dialog = False
|
||||
|
||||
|
||||
def _draw_save_as_project_dialog(self):
|
||||
"""绘制另存为项目对话框。"""
|
||||
if not self.show_save_as_dialog:
|
||||
return
|
||||
|
||||
if not getattr(self, "save_as_project_name", "").strip():
|
||||
current_project_path = getattr(getattr(self, "project_manager", None), "current_project_path", None)
|
||||
default_name = os.path.basename(current_project_path) if current_project_path else "新项目"
|
||||
self.save_as_project_name = f"{default_name}_副本"
|
||||
if not getattr(self, "save_as_project_path", "").strip():
|
||||
current_project_path = getattr(getattr(self, "project_manager", None), "current_project_path", None)
|
||||
self.save_as_project_path = os.path.dirname(current_project_path) if current_project_path else os.getcwd()
|
||||
|
||||
flags = (
|
||||
imgui.WindowFlags_.no_resize
|
||||
| imgui.WindowFlags_.no_collapse
|
||||
| imgui.WindowFlags_.modal
|
||||
)
|
||||
self.style_manager.prepare_centered_dialog(460, 240)
|
||||
|
||||
with imgui_ctx.begin("项目另存为", True, flags) as window:
|
||||
if not window.opened:
|
||||
self.show_save_as_dialog = False
|
||||
return
|
||||
|
||||
imgui.text("将当前项目保存到新的项目目录")
|
||||
imgui.separator()
|
||||
|
||||
changed, project_name = imgui.input_text("项目名称", self.save_as_project_name, 256)
|
||||
if changed:
|
||||
self.save_as_project_name = project_name
|
||||
|
||||
changed, project_path = imgui.input_text("保存位置", self.save_as_project_path, 512)
|
||||
if changed:
|
||||
self.save_as_project_path = project_path
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("浏览..."):
|
||||
self.path_browser_mode = "save_as_project"
|
||||
self.path_browser_current_path = self.save_as_project_path if self.save_as_project_path else os.getcwd()
|
||||
self.show_path_browser = True
|
||||
self._refresh_path_browser()
|
||||
|
||||
imgui.separator()
|
||||
target_path = os.path.normpath(os.path.join(self.save_as_project_path or "", self.save_as_project_name or ""))
|
||||
imgui.text("目标路径:")
|
||||
imgui.same_line()
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), target_path)
|
||||
|
||||
if imgui.button("保存"):
|
||||
if self.save_as_project_name.strip() and self.save_as_project_path.strip():
|
||||
if self._save_project_as_impl(self.save_as_project_name.strip(), self.save_as_project_path.strip()):
|
||||
self.show_save_as_dialog = False
|
||||
else:
|
||||
self.add_warning_message("项目名称和保存位置不能为空")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("取消"):
|
||||
self.show_save_as_dialog = False
|
||||
|
||||
|
||||
def _draw_path_browser(self):
|
||||
@ -383,6 +446,10 @@ class DialogPanels:
|
||||
# 打开项目模式:使用当前路径
|
||||
self.open_project_path = self.path_browser_current_path
|
||||
self.add_info_message(f"已选择项目路径: {self.open_project_path}")
|
||||
elif self.path_browser_mode == "save_as_project":
|
||||
# 另存为项目模式:使用当前路径作为目标目录
|
||||
self.save_as_project_path = self.path_browser_current_path
|
||||
self.add_info_message(f"已选择保存位置: {self.save_as_project_path}")
|
||||
elif self.path_browser_mode == "import_model":
|
||||
# 导入模型模式:使用选择的文件路径
|
||||
self.import_file_path = self.path_browser_selected_path
|
||||
@ -391,6 +458,43 @@ class DialogPanels:
|
||||
self.add_error_message(f"应用路径失败: {e}")
|
||||
|
||||
|
||||
def _draw_about_dialog(self):
|
||||
"""绘制关于对话框。"""
|
||||
if not self.show_about_dialog:
|
||||
return
|
||||
|
||||
flags = (
|
||||
imgui.WindowFlags_.no_resize
|
||||
| imgui.WindowFlags_.no_collapse
|
||||
| imgui.WindowFlags_.modal
|
||||
)
|
||||
self.style_manager.prepare_centered_dialog(460, 260)
|
||||
|
||||
with imgui_ctx.begin("关于 EG", True, flags) as window:
|
||||
if not window.opened:
|
||||
self.show_about_dialog = False
|
||||
return
|
||||
|
||||
current_project_path = getattr(getattr(self, "project_manager", None), "current_project_path", None) or "未打开项目"
|
||||
imgui.text("EG 编辑器")
|
||||
imgui.separator()
|
||||
imgui.text(f"Python: {platform.python_version()}")
|
||||
imgui.text(f"平台: {platform.system()} {platform.release()}")
|
||||
imgui.text(f"可执行文件: {os.path.basename(sys.executable)}")
|
||||
imgui.separator()
|
||||
imgui.text("当前项目:")
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), current_project_path)
|
||||
imgui.separator()
|
||||
imgui.text_wrapped("当前帮助菜单的文档入口会打开项目目录下的本地 Markdown 文档。")
|
||||
|
||||
if imgui.button("打开文档"):
|
||||
self._open_documentation()
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("关闭"):
|
||||
self.show_about_dialog = False
|
||||
|
||||
|
||||
def _draw_spot_light_dialog(self):
|
||||
"""绘制聚光灯创建对话框"""
|
||||
if not self.show_spot_light_dialog:
|
||||
|
||||
@ -12,6 +12,67 @@ class EditorPanelsRightMixin(
|
||||
):
|
||||
"""Right panel aggregator mixin."""
|
||||
|
||||
def _record_visibility_change(self, node, old_visible, new_visible):
|
||||
if not hasattr(self.app, "command_manager") or not self.app.command_manager:
|
||||
node.setPythonTag("user_visible", bool(new_visible))
|
||||
if new_visible:
|
||||
node.show()
|
||||
else:
|
||||
node.hide()
|
||||
return
|
||||
from core.Command_System import VisibilityNodeCommand
|
||||
self.app.command_manager.execute_command(
|
||||
VisibilityNodeCommand(node, old_visible, new_visible)
|
||||
)
|
||||
|
||||
def _record_name_change(self, node, old_name, new_name):
|
||||
if old_name == new_name:
|
||||
return
|
||||
if hasattr(self.app, "command_manager") and self.app.command_manager:
|
||||
from core.Command_System import RenameNodeCommand
|
||||
self.app.command_manager.record_command(
|
||||
RenameNodeCommand(node, old_name, new_name, world=self.app)
|
||||
)
|
||||
else:
|
||||
self.app._update_node_name(node, new_name)
|
||||
|
||||
def _ensure_light_edit_sessions(self):
|
||||
if not hasattr(self, "_light_edit_sessions"):
|
||||
self._light_edit_sessions = {}
|
||||
return self._light_edit_sessions
|
||||
|
||||
def _begin_light_edit_session(self, node, session_key):
|
||||
sessions = self._ensure_light_edit_sessions()
|
||||
sessions.setdefault(session_key, self.app._capture_light_snapshot(node))
|
||||
|
||||
def _record_light_snapshot_command(self, node, before_snapshot, after_snapshot):
|
||||
if not hasattr(self.app, "command_manager") or not self.app.command_manager:
|
||||
return
|
||||
if self.app._light_snapshots_equal(before_snapshot, after_snapshot):
|
||||
return
|
||||
try:
|
||||
from core.Command_System import SnapshotStateCommand
|
||||
|
||||
self.app.command_manager.record_command(
|
||||
SnapshotStateCommand(
|
||||
lambda state, target_node=node: self.app._apply_light_snapshot(target_node, state),
|
||||
before_snapshot,
|
||||
after_snapshot,
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _finish_light_edit_session(self, node, session_key):
|
||||
if not imgui.is_item_deactivated_after_edit():
|
||||
return
|
||||
sessions = self._ensure_light_edit_sessions()
|
||||
before_snapshot = sessions.pop(session_key, None)
|
||||
if before_snapshot is None:
|
||||
return
|
||||
after_snapshot = self.app._capture_light_snapshot(node)
|
||||
self._record_light_snapshot_command(node, before_snapshot, after_snapshot)
|
||||
|
||||
def _draw_property_panel(self):
|
||||
"""绘制属性面板"""
|
||||
# 使用面板类型的窗口标志,支持docking
|
||||
@ -120,11 +181,7 @@ class EditorPanelsRightMixin(
|
||||
|
||||
changed, is_visible = imgui.checkbox("##node_enabled", user_visible)
|
||||
if changed:
|
||||
node.setPythonTag("user_visible", is_visible)
|
||||
if is_visible:
|
||||
node.show()
|
||||
else:
|
||||
node.hide()
|
||||
self._record_visibility_change(node, user_visible, is_visible)
|
||||
|
||||
imgui.same_line()
|
||||
imgui.text_disabled("启用")
|
||||
@ -132,7 +189,17 @@ class EditorPanelsRightMixin(
|
||||
imgui.set_next_item_width(-1)
|
||||
changed, new_name = imgui.input_text("##name", node_name, 256)
|
||||
if changed and hasattr(self.app, "selection"):
|
||||
if not hasattr(self.app, "_name_edit_session"):
|
||||
self.app._name_edit_session = None
|
||||
if not self.app._name_edit_session or self.app._name_edit_session[0] != node:
|
||||
self.app._name_edit_session = (node, node_name)
|
||||
self.app._update_node_name(node, new_name)
|
||||
if imgui.is_item_deactivated_after_edit():
|
||||
session = getattr(self.app, "_name_edit_session", None)
|
||||
if session and session[0] == node:
|
||||
_, original_name = session
|
||||
self._record_name_change(node, original_name, node.getName())
|
||||
self.app._name_edit_session = None
|
||||
|
||||
parent_name = "SceneRoot"
|
||||
try:
|
||||
@ -388,26 +455,19 @@ class EditorPanelsRightMixin(
|
||||
def _draw_light_properties(self, node):
|
||||
"""绘制光源属性"""
|
||||
imgui.text("光源属性")
|
||||
|
||||
# 光源颜色
|
||||
if hasattr(node, 'getColor'):
|
||||
try:
|
||||
color = node.getColor()
|
||||
# 确保颜色是有效的
|
||||
if not color or len(color) < 3:
|
||||
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||||
except:
|
||||
color = (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||||
|
||||
changed, new_r = imgui.drag_float("颜色 R", color[0], 0.01, 0.0, 1.0)
|
||||
if changed: node.setColor(new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0)
|
||||
|
||||
changed, new_g = imgui.drag_float("颜色 G", color[1], 0.01, 0.0, 1.0)
|
||||
if changed: node.setColor(color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0)
|
||||
|
||||
changed, new_b = imgui.drag_float("颜色 B", color[2], 0.01, 0.0, 1.0)
|
||||
if changed: node.setColor(color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0)
|
||||
|
||||
|
||||
color = self.app._get_light_color(node)
|
||||
changed, new_color = imgui.color_edit3(
|
||||
"颜色##light_color",
|
||||
(color[0], color[1], color[2]),
|
||||
imgui.ColorEditFlags_.display_rgb.value,
|
||||
)
|
||||
if imgui.is_item_activated():
|
||||
self._begin_light_edit_session(node, "light_color")
|
||||
if changed:
|
||||
self.app._apply_light_color(node, (new_color[0], new_color[1], new_color[2], color[3]))
|
||||
self._finish_light_edit_session(node, "light_color")
|
||||
|
||||
# 光源强度
|
||||
imgui.text("光源强度: (暂不支持编辑)")
|
||||
|
||||
|
||||
@ -3,6 +3,49 @@ from imgui_bundle import imgui, imgui_ctx
|
||||
class EditorPanelsRightCollisionMixin:
|
||||
"""Auto-split mixin from editor_panels_right.py."""
|
||||
|
||||
def _ensure_collision_edit_sessions(self):
|
||||
if not hasattr(self, "_collision_edit_sessions"):
|
||||
self._collision_edit_sessions = {}
|
||||
return self._collision_edit_sessions
|
||||
|
||||
def _begin_collision_edit_session(self, node, session_key):
|
||||
sessions = self._ensure_collision_edit_sessions()
|
||||
sessions.setdefault(session_key, self.app._capture_collision_snapshot(node))
|
||||
|
||||
def _record_collision_snapshot_command(self, node, before_snapshot, after_snapshot):
|
||||
if not hasattr(self.app, "command_manager") or not self.app.command_manager:
|
||||
return
|
||||
if self.app._collision_snapshots_equal(before_snapshot, after_snapshot):
|
||||
return
|
||||
try:
|
||||
from core.Command_System import SnapshotStateCommand
|
||||
|
||||
self.app.command_manager.record_command(
|
||||
SnapshotStateCommand(
|
||||
lambda state, target_node=node: self.app._apply_collision_snapshot(target_node, state),
|
||||
before_snapshot,
|
||||
after_snapshot,
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _finish_collision_edit_session(self, node, session_key):
|
||||
if not imgui.is_item_deactivated_after_edit():
|
||||
return
|
||||
sessions = self._ensure_collision_edit_sessions()
|
||||
before_snapshot = sessions.pop(session_key, None)
|
||||
if before_snapshot is None:
|
||||
return
|
||||
after_snapshot = self.app._capture_collision_snapshot(node)
|
||||
self._record_collision_snapshot_command(node, before_snapshot, after_snapshot)
|
||||
|
||||
def _apply_collision_change_with_history(self, node, apply_callback):
|
||||
before_snapshot = self.app._capture_collision_snapshot(node)
|
||||
apply_callback()
|
||||
after_snapshot = self.app._capture_collision_snapshot(node)
|
||||
self._record_collision_snapshot_command(node, before_snapshot, after_snapshot)
|
||||
|
||||
def _draw_collision_properties(self, node):
|
||||
"""绘制碰撞检测属性"""
|
||||
if not node or node.isEmpty():
|
||||
@ -37,13 +80,14 @@ class EditorPanelsRightCollisionMixin:
|
||||
current_index = collision_shapes.index(current_shape) if current_shape in collision_shapes else 0
|
||||
changed, selected_index = imgui.combo("##collision_shape", current_index, collision_shapes)
|
||||
if changed:
|
||||
# 始终更新选择的形状
|
||||
selected_shape = collision_shapes[selected_index]
|
||||
self.app._selected_collision_shape = selected_shape
|
||||
# 如果已经有碰撞体,询问用户是否要重新创建
|
||||
if has_collision:
|
||||
self._remove_collision_from_node(node)
|
||||
self._add_collision_to_node(node)
|
||||
self._apply_collision_change_with_history(
|
||||
node,
|
||||
lambda: self._change_collision_shape(node, selected_shape),
|
||||
)
|
||||
else:
|
||||
self.app._selected_collision_shape = selected_shape
|
||||
|
||||
imgui.separator()
|
||||
|
||||
@ -55,18 +99,27 @@ class EditorPanelsRightCollisionMixin:
|
||||
|
||||
# X位置
|
||||
changed, new_x = imgui.drag_float("X##collision_pos_x", pos_offset[0], 0.1, -100.0, 100.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_position")
|
||||
if changed and has_collision:
|
||||
self._update_collision_position(node, 'x', new_x)
|
||||
self._finish_collision_edit_session(node, "collision_position")
|
||||
|
||||
# Y位置
|
||||
changed, new_y = imgui.drag_float("Y##collision_pos_y", pos_offset[1], 0.1, -100.0, 100.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_position")
|
||||
if changed and has_collision:
|
||||
self._update_collision_position(node, 'y', new_y)
|
||||
self._finish_collision_edit_session(node, "collision_position")
|
||||
|
||||
# Z位置
|
||||
changed, new_z = imgui.drag_float("Z##collision_pos_z", pos_offset[2], 0.1, -100.0, 100.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_position")
|
||||
if changed and has_collision:
|
||||
self._update_collision_position(node, 'z', new_z)
|
||||
self._finish_collision_edit_session(node, "collision_position")
|
||||
|
||||
# 形状特定参数(始终显示,但根据状态启用/禁用)
|
||||
shape_type = self._get_current_collision_shape_type(node)
|
||||
@ -80,17 +133,17 @@ class EditorPanelsRightCollisionMixin:
|
||||
is_visible = self._is_collision_visible(node)
|
||||
visibility_text = "隐藏碰撞体" if is_visible else "显示碰撞体"
|
||||
if imgui.button(visibility_text):
|
||||
self._toggle_collision_visibility(node)
|
||||
self._apply_collision_change_with_history(node, lambda: self._toggle_collision_visibility(node))
|
||||
|
||||
imgui.same_line()
|
||||
|
||||
# 移除碰撞按钮
|
||||
if imgui.button("移除碰撞"):
|
||||
self._remove_collision_from_node(node)
|
||||
self._apply_collision_change_with_history(node, lambda: self._remove_collision_from_node(node))
|
||||
else:
|
||||
# 添加碰撞按钮
|
||||
if imgui.button("添加碰撞"):
|
||||
self._add_collision_to_node(node)
|
||||
self._apply_collision_change_with_history(node, lambda: self._add_collision_to_node(node))
|
||||
|
||||
imgui.separator()
|
||||
|
||||
@ -115,6 +168,23 @@ class EditorPanelsRightCollisionMixin:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _change_collision_shape(self, node, selected_shape):
|
||||
self.app._selected_collision_shape = selected_shape
|
||||
offset = self._get_collision_position_offset(node)
|
||||
visible = self._is_collision_visible(node)
|
||||
self._remove_collision_from_node(node)
|
||||
self._add_collision_to_node(node)
|
||||
collision_node = self.app.property_helpers._get_collision_node(node)
|
||||
if collision_node:
|
||||
try:
|
||||
collision_node.setPos(*offset)
|
||||
except Exception:
|
||||
pass
|
||||
if visible:
|
||||
collision_node.show()
|
||||
else:
|
||||
collision_node.hide()
|
||||
|
||||
def _draw_shape_specific_parameters(self, node, shape_type, has_collision=True):
|
||||
"""绘制形状特定参数"""
|
||||
try:
|
||||
@ -140,8 +210,11 @@ class EditorPanelsRightCollisionMixin:
|
||||
|
||||
# 半径调整
|
||||
changed, new_radius = imgui.drag_float("半径##sphere_radius", radius, 0.1, 0.1, 100.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_sphere_radius")
|
||||
if changed and has_collision:
|
||||
self._update_sphere_radius(node, new_radius)
|
||||
self._finish_collision_edit_session(node, "collision_sphere_radius")
|
||||
|
||||
except Exception as e:
|
||||
print(f"绘制球形参数失败: {e}")
|
||||
@ -156,16 +229,25 @@ class EditorPanelsRightCollisionMixin:
|
||||
|
||||
# 尺寸调整
|
||||
changed, new_x = imgui.drag_float("长度##box_length", size[0], 0.1, 0.1, 100.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_box_size")
|
||||
if changed and has_collision:
|
||||
self._update_box_size(node, 'x', new_x)
|
||||
self._finish_collision_edit_session(node, "collision_box_size")
|
||||
|
||||
changed, new_y = imgui.drag_float("宽度##box_width", size[1], 0.1, 0.1, 100.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_box_size")
|
||||
if changed and has_collision:
|
||||
self._update_box_size(node, 'y', new_y)
|
||||
self._finish_collision_edit_session(node, "collision_box_size")
|
||||
|
||||
changed, new_z = imgui.drag_float("高度##box_height", size[2], 0.1, 0.1, 100.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_box_size")
|
||||
if changed and has_collision:
|
||||
self._update_box_size(node, 'z', new_z)
|
||||
self._finish_collision_edit_session(node, "collision_box_size")
|
||||
|
||||
except Exception as e:
|
||||
print(f"绘制盒型参数失败: {e}")
|
||||
@ -181,13 +263,19 @@ class EditorPanelsRightCollisionMixin:
|
||||
|
||||
# 半径调整
|
||||
changed, new_radius = imgui.drag_float("半径##capsule_radius", radius, 0.1, 0.1, 100.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_capsule_radius")
|
||||
if changed and has_collision:
|
||||
self._update_capsule_radius(node, new_radius)
|
||||
self._finish_collision_edit_session(node, "collision_capsule_radius")
|
||||
|
||||
# 高度调整
|
||||
changed, new_height = imgui.drag_float("高度##capsule_height", height, 0.1, 0.1, 100.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_capsule_height")
|
||||
if changed and has_collision:
|
||||
self._update_capsule_height(node, new_height)
|
||||
self._finish_collision_edit_session(node, "collision_capsule_height")
|
||||
|
||||
except Exception as e:
|
||||
print(f"绘制胶囊体参数失败: {e}")
|
||||
@ -202,16 +290,25 @@ class EditorPanelsRightCollisionMixin:
|
||||
|
||||
# 法向量调整
|
||||
changed, new_x = imgui.drag_float("法向量 X##plane_normal_x", normal[0], 0.1, -1.0, 1.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_plane_normal")
|
||||
if changed and has_collision:
|
||||
self._update_plane_normal(node, 'x', new_x)
|
||||
self._finish_collision_edit_session(node, "collision_plane_normal")
|
||||
|
||||
changed, new_y = imgui.drag_float("法向量 Y##plane_normal_y", normal[1], 0.1, -1.0, 1.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_plane_normal")
|
||||
if changed and has_collision:
|
||||
self._update_plane_normal(node, 'y', new_y)
|
||||
self._finish_collision_edit_session(node, "collision_plane_normal")
|
||||
|
||||
changed, new_z = imgui.drag_float("法向量 Z##plane_normal_z", normal[2], 0.1, -1.0, 1.0, "%.2f")
|
||||
if imgui.is_item_activated() and has_collision:
|
||||
self._begin_collision_edit_session(node, "collision_plane_normal")
|
||||
if changed and has_collision:
|
||||
self._update_plane_normal(node, 'z', new_z)
|
||||
self._finish_collision_edit_session(node, "collision_plane_normal")
|
||||
|
||||
except Exception as e:
|
||||
print(f"绘制平面参数失败: {e}")
|
||||
|
||||
@ -3,6 +3,50 @@ from imgui_bundle import imgui, imgui_ctx
|
||||
class EditorPanelsRightMaterialMixin:
|
||||
"""Auto-split mixin from editor_panels_right.py."""
|
||||
|
||||
def _ensure_material_edit_sessions(self):
|
||||
if not hasattr(self, "_material_edit_sessions"):
|
||||
self._material_edit_sessions = {}
|
||||
return self._material_edit_sessions
|
||||
|
||||
def _begin_material_edit_session(self, node, session_key):
|
||||
sessions = self._ensure_material_edit_sessions()
|
||||
sessions.setdefault(session_key, self.app._capture_node_material_snapshot(node))
|
||||
|
||||
def _record_material_snapshot_command(self, node, before_snapshot, after_snapshot):
|
||||
if not hasattr(self.app, "command_manager") or not self.app.command_manager:
|
||||
return
|
||||
if self.app._material_snapshots_equal(before_snapshot, after_snapshot):
|
||||
return
|
||||
|
||||
try:
|
||||
from core.Command_System import MaterialStateCommand
|
||||
|
||||
self.app.command_manager.record_command(
|
||||
MaterialStateCommand(
|
||||
lambda state, target_node=node: self.app._apply_node_material_snapshot(target_node, state),
|
||||
before_snapshot,
|
||||
after_snapshot,
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _finish_material_edit_session(self, node, session_key):
|
||||
if not imgui.is_item_deactivated_after_edit():
|
||||
return
|
||||
sessions = self._ensure_material_edit_sessions()
|
||||
before_snapshot = sessions.pop(session_key, None)
|
||||
if before_snapshot is None:
|
||||
return
|
||||
after_snapshot = self.app._capture_node_material_snapshot(node)
|
||||
self._record_material_snapshot_command(node, before_snapshot, after_snapshot)
|
||||
|
||||
def _apply_material_change_with_history(self, node, apply_callback):
|
||||
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 _draw_appearance_properties(self, node):
|
||||
"""绘制材质属性(Unity风格主材质入口)。"""
|
||||
materials = self.app._get_node_materials(node)
|
||||
@ -56,8 +100,11 @@ class EditorPanelsRightMaterialMixin:
|
||||
base_color,
|
||||
imgui.ColorEditFlags_.display_rgb.value,
|
||||
)
|
||||
if imgui.is_item_activated():
|
||||
self._begin_material_edit_session(node, "appearance_primary_color")
|
||||
if changed:
|
||||
apply_primary_color(new_color)
|
||||
self._finish_material_edit_session(node, "appearance_primary_color")
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("颜色选择器##material_color_picker"):
|
||||
@ -65,7 +112,10 @@ class EditorPanelsRightMaterialMixin:
|
||||
target_object=None,
|
||||
property_name=None,
|
||||
initial_color=base_color,
|
||||
callback=apply_primary_color,
|
||||
callback=lambda color: self._apply_material_change_with_history(
|
||||
node,
|
||||
lambda: apply_primary_color(color),
|
||||
),
|
||||
)
|
||||
|
||||
surface_options = [
|
||||
@ -86,15 +136,21 @@ class EditorPanelsRightMaterialMixin:
|
||||
[label for label, _ in surface_options],
|
||||
)
|
||||
if changed:
|
||||
apply_surface_type(surface_options[selected_index][1])
|
||||
self._apply_material_change_with_history(
|
||||
node,
|
||||
lambda: apply_surface_type(surface_options[selected_index][1]),
|
||||
)
|
||||
current_surface = surface_options[selected_index][1]
|
||||
|
||||
if self.app._get_material_surface_type(material) == 3:
|
||||
opacity = self.app._get_material_opacity(material)
|
||||
transparency = 1.0 - opacity
|
||||
changed, new_transparency = imgui.slider_float("透明度", transparency, 0.0, 1.0)
|
||||
if imgui.is_item_activated():
|
||||
self._begin_material_edit_session(node, "appearance_opacity")
|
||||
if changed:
|
||||
apply_opacity(1.0 - new_transparency)
|
||||
self._finish_material_edit_session(node, "appearance_opacity")
|
||||
|
||||
imgui.separator()
|
||||
|
||||
@ -119,8 +175,11 @@ class EditorPanelsRightMaterialMixin:
|
||||
try:
|
||||
roughness_value = float(material.roughness)
|
||||
changed, new_roughness = imgui.slider_float(f"粗糙度##rough_{i}", roughness_value, 0.0, 1.0)
|
||||
if imgui.is_item_activated():
|
||||
self._begin_material_edit_session(node, f"material_{i}_roughness")
|
||||
if changed:
|
||||
self._update_material_roughness(material, new_roughness)
|
||||
self._finish_material_edit_session(node, f"material_{i}_roughness")
|
||||
except:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "粗糙度: 不可用")
|
||||
|
||||
@ -128,8 +187,11 @@ class EditorPanelsRightMaterialMixin:
|
||||
try:
|
||||
metallic_value = float(material.metallic)
|
||||
changed, new_metallic = imgui.slider_float(f"金属性##metal_{i}", metallic_value, 0.0, 1.0)
|
||||
if imgui.is_item_activated():
|
||||
self._begin_material_edit_session(node, f"material_{i}_metallic")
|
||||
if changed:
|
||||
self._update_material_metallic(material, new_metallic)
|
||||
self._finish_material_edit_session(node, f"material_{i}_metallic")
|
||||
except:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "金属性: 不可用")
|
||||
|
||||
@ -137,8 +199,11 @@ class EditorPanelsRightMaterialMixin:
|
||||
try:
|
||||
ior_value = float(material.refractive_index)
|
||||
changed, new_ior = imgui.slider_float(f"折射率##ior_{i}", ior_value, 1.0, 3.0)
|
||||
if imgui.is_item_activated():
|
||||
self._begin_material_edit_session(node, f"material_{i}_ior")
|
||||
if changed:
|
||||
self._update_material_ior(material, new_ior)
|
||||
self._finish_material_edit_session(node, f"material_{i}_ior")
|
||||
except:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "折射率: 不可用")
|
||||
|
||||
@ -150,8 +215,14 @@ class EditorPanelsRightMaterialMixin:
|
||||
if imgui.begin_combo(f"预设##preset_{i}", presets[current_preset]):
|
||||
for j, preset_name in enumerate(presets):
|
||||
if imgui.selectable(preset_name, j == current_preset):
|
||||
self._apply_material_preset(material, preset_name)
|
||||
self._apply_material_surface_state(node, material)
|
||||
self._apply_material_change_with_history(
|
||||
node,
|
||||
lambda selected_preset=preset_name: (
|
||||
self._apply_material_preset(material, selected_preset),
|
||||
self._apply_material_surface_state(node, material),
|
||||
self.app._refresh_pipeline_material_mode(node, material),
|
||||
),
|
||||
)
|
||||
imgui.end_combo()
|
||||
|
||||
# 纹理信息
|
||||
@ -186,7 +257,7 @@ class EditorPanelsRightMaterialMixin:
|
||||
self._apply_material_to_node(node)
|
||||
imgui.same_line()
|
||||
if imgui.button("重置材质"):
|
||||
self._reset_material(node)
|
||||
self._apply_material_change_with_history(node, lambda: self._reset_material(node))
|
||||
|
||||
def _draw_shading_model_panel(self, node, material, material_index):
|
||||
"""绘制着色模型选择面板"""
|
||||
|
||||
@ -4,6 +4,33 @@ from imgui_bundle import imgui
|
||||
class EditorPanelsRightTransformMixin:
|
||||
"""Compact Unity-style transform editor."""
|
||||
|
||||
def _ensure_transform_edit_sessions(self):
|
||||
if not hasattr(self, "_transform_edit_sessions"):
|
||||
self._transform_edit_sessions = {}
|
||||
return self._transform_edit_sessions
|
||||
|
||||
def _record_transform_property_command(self, node, row_id, old_values, new_values):
|
||||
if not hasattr(self, "command_manager") or not self.command_manager:
|
||||
return
|
||||
if tuple(round(float(v), 6) for v in old_values) == tuple(round(float(v), 6) for v in new_values):
|
||||
return
|
||||
|
||||
try:
|
||||
from core.Command_System import MoveNodeCommand, RotateNodeCommand, ScaleNodeCommand
|
||||
|
||||
command = None
|
||||
if row_id == "position":
|
||||
command = MoveNodeCommand(node, tuple(old_values), tuple(new_values))
|
||||
elif row_id == "rotation":
|
||||
command = RotateNodeCommand(node, tuple(old_values), tuple(new_values))
|
||||
elif row_id == "scale":
|
||||
command = ScaleNodeCommand(node, tuple(old_values), tuple(new_values))
|
||||
|
||||
if command is not None:
|
||||
self.command_manager.record_command(command)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _draw_transform_properties(self, node):
|
||||
imgui.push_style_var(imgui.StyleVar_.frame_padding, (6.0, 4.0))
|
||||
imgui.push_style_var(imgui.StyleVar_.item_spacing, (6.0, 4.0))
|
||||
@ -13,6 +40,7 @@ class EditorPanelsRightTransformMixin:
|
||||
scale = node.getScale()
|
||||
|
||||
self._draw_transform_row(
|
||||
node,
|
||||
"位置",
|
||||
"position",
|
||||
(position.x, position.y, position.z),
|
||||
@ -22,6 +50,7 @@ class EditorPanelsRightTransformMixin:
|
||||
reset_values=(0.0, 0.0, 0.0),
|
||||
)
|
||||
self._draw_transform_row(
|
||||
node,
|
||||
"旋转",
|
||||
"rotation",
|
||||
(rotation.x, rotation.y, rotation.z),
|
||||
@ -31,6 +60,7 @@ class EditorPanelsRightTransformMixin:
|
||||
reset_values=(0.0, 0.0, 0.0),
|
||||
)
|
||||
self._draw_transform_row(
|
||||
node,
|
||||
"缩放",
|
||||
"scale",
|
||||
(scale.x, scale.y, scale.z),
|
||||
@ -46,6 +76,7 @@ class EditorPanelsRightTransformMixin:
|
||||
|
||||
def _draw_transform_row(
|
||||
self,
|
||||
node,
|
||||
label,
|
||||
row_id,
|
||||
values,
|
||||
@ -65,6 +96,8 @@ class EditorPanelsRightTransformMixin:
|
||||
current_values = [float(value) for value in values]
|
||||
original_values = list(current_values)
|
||||
changed_any = False
|
||||
edit_finished = False
|
||||
sessions = self._ensure_transform_edit_sessions()
|
||||
|
||||
table_flags = (
|
||||
imgui.TableFlags_.sizing_stretch_same.value
|
||||
@ -106,6 +139,7 @@ class EditorPanelsRightTransformMixin:
|
||||
format=value_format,
|
||||
)
|
||||
if axis_changed:
|
||||
sessions.setdefault(row_id, list(original_values))
|
||||
current_values = self._apply_vector_value_change(
|
||||
original_values,
|
||||
current_values,
|
||||
@ -115,12 +149,18 @@ class EditorPanelsRightTransformMixin:
|
||||
reset_values,
|
||||
)
|
||||
changed_any = True
|
||||
if imgui.is_item_deactivated_after_edit():
|
||||
edit_finished = True
|
||||
|
||||
imgui.end_table()
|
||||
|
||||
if changed_any:
|
||||
apply_callback(tuple(current_values))
|
||||
|
||||
if edit_finished:
|
||||
initial_values = sessions.pop(row_id, list(original_values))
|
||||
self._record_transform_property_command(node, row_id, initial_values, current_values)
|
||||
|
||||
def _apply_vector_value_change(
|
||||
self,
|
||||
original_values,
|
||||
|
||||
@ -245,8 +245,10 @@ class EditorPanelsTopMixin:
|
||||
# 帮助菜单
|
||||
with imgui_ctx.begin_menu("帮助") as help_menu:
|
||||
if help_menu:
|
||||
imgui.menu_item("关于", "", False, True)
|
||||
imgui.menu_item("文档", "", False, True)
|
||||
if imgui.menu_item("关于", "", False, True)[1]:
|
||||
self.app._show_about_dialog()
|
||||
if imgui.menu_item("文档", "", False, True)[1]:
|
||||
self.app._open_documentation()
|
||||
|
||||
# 右侧显示FPS
|
||||
imgui.set_cursor_pos_x(imgui.get_window_size().x - 140)
|
||||
|
||||
@ -187,6 +187,15 @@ class PanelDelegates:
|
||||
def _get_current_collision_shape_type(self, *args, **kwargs):
|
||||
return self.property_helpers._get_current_collision_shape_type(*args, **kwargs)
|
||||
|
||||
def _capture_collision_snapshot(self, *args, **kwargs):
|
||||
return self.property_helpers._capture_collision_snapshot(*args, **kwargs)
|
||||
|
||||
def _collision_snapshots_equal(self, *args, **kwargs):
|
||||
return self.property_helpers._collision_snapshots_equal(*args, **kwargs)
|
||||
|
||||
def _apply_collision_snapshot(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_collision_snapshot(*args, **kwargs)
|
||||
|
||||
def _get_collision_position_offset(self, *args, **kwargs):
|
||||
return self.property_helpers._get_collision_position_offset(*args, **kwargs)
|
||||
|
||||
@ -244,9 +253,33 @@ class PanelDelegates:
|
||||
def _update_node_name(self, *args, **kwargs):
|
||||
return self.property_helpers._update_node_name(*args, **kwargs)
|
||||
|
||||
def _get_light_color(self, *args, **kwargs):
|
||||
return self.property_helpers._get_light_color(*args, **kwargs)
|
||||
|
||||
def _apply_light_color(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_light_color(*args, **kwargs)
|
||||
|
||||
def _capture_light_snapshot(self, *args, **kwargs):
|
||||
return self.property_helpers._capture_light_snapshot(*args, **kwargs)
|
||||
|
||||
def _light_snapshots_equal(self, *args, **kwargs):
|
||||
return self.property_helpers._light_snapshots_equal(*args, **kwargs)
|
||||
|
||||
def _apply_light_snapshot(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_light_snapshot(*args, **kwargs)
|
||||
|
||||
def _get_material_base_color(self, *args, **kwargs):
|
||||
return self.property_helpers._get_material_base_color(*args, **kwargs)
|
||||
|
||||
def _capture_node_material_snapshot(self, *args, **kwargs):
|
||||
return self.property_helpers._capture_node_material_snapshot(*args, **kwargs)
|
||||
|
||||
def _material_snapshots_equal(self, *args, **kwargs):
|
||||
return self.property_helpers._material_snapshots_equal(*args, **kwargs)
|
||||
|
||||
def _apply_node_material_snapshot(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_node_material_snapshot(*args, **kwargs)
|
||||
|
||||
def _get_node_materials(self, *args, **kwargs):
|
||||
return self.property_helpers._get_node_materials(*args, **kwargs)
|
||||
|
||||
@ -418,6 +451,12 @@ class PanelDelegates:
|
||||
def _on_save_as_project(self, *args, **kwargs):
|
||||
return self.app_actions._on_save_as_project(*args, **kwargs)
|
||||
|
||||
def _show_about_dialog(self, *args, **kwargs):
|
||||
return self.app_actions._show_about_dialog(*args, **kwargs)
|
||||
|
||||
def _open_documentation(self, *args, **kwargs):
|
||||
return self.app_actions._open_documentation(*args, **kwargs)
|
||||
|
||||
def _on_exit(self, *args, **kwargs):
|
||||
return self.app_actions._on_exit(*args, **kwargs)
|
||||
|
||||
@ -514,6 +553,9 @@ class PanelDelegates:
|
||||
def _create_new_project_impl(self, *args, **kwargs):
|
||||
return self.app_actions._create_new_project_impl(*args, **kwargs)
|
||||
|
||||
def _save_project_as_impl(self, *args, **kwargs):
|
||||
return self.app_actions._save_project_as_impl(*args, **kwargs)
|
||||
|
||||
def _update_window_title(self, *args, **kwargs):
|
||||
return self.app_actions._update_window_title(*args, **kwargs)
|
||||
|
||||
@ -535,6 +577,9 @@ class PanelDelegates:
|
||||
def _draw_open_project_dialog(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_open_project_dialog(*args, **kwargs)
|
||||
|
||||
def _draw_save_as_project_dialog(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_save_as_project_dialog(*args, **kwargs)
|
||||
|
||||
def _draw_path_browser(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_path_browser(*args, **kwargs)
|
||||
|
||||
@ -571,6 +616,9 @@ class PanelDelegates:
|
||||
def _refresh_heightmap_browser(self, *args, **kwargs):
|
||||
return self.dialog_panels._refresh_heightmap_browser(*args, **kwargs)
|
||||
|
||||
def _draw_about_dialog(self, *args, **kwargs):
|
||||
return self.dialog_panels._draw_about_dialog(*args, **kwargs)
|
||||
|
||||
def setup_drag_drop_support(self):
|
||||
"""初始化拖拽支持。"""
|
||||
return self.model_drag_drop.setup_drag_drop_support()
|
||||
|
||||
@ -130,6 +130,20 @@ class PropertyHelpers:
|
||||
except Exception as e:
|
||||
print(f"检查碰撞状态失败: {e}")
|
||||
return False
|
||||
|
||||
def _get_collision_node(self, node):
|
||||
"""Return the first collision child node for a scene node."""
|
||||
try:
|
||||
if not node or node.isEmpty():
|
||||
return None
|
||||
for child in node.getChildren():
|
||||
if hasattr(child, 'getName') and child.getName():
|
||||
name = child.getName()
|
||||
if 'collision' in name.lower() or 'Collision' in name:
|
||||
return child
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _get_current_collision_shape(self, node):
|
||||
@ -246,11 +260,7 @@ class PropertyHelpers:
|
||||
if hasattr(self, 'collision_manager'):
|
||||
# 使用碰撞管理器添加碰撞体
|
||||
shape_type = self._get_shape_type_from_name(shape_name)
|
||||
collision_node = self.collision_manager.setupAdvancedCollision(
|
||||
node,
|
||||
shape_type=shape_type,
|
||||
mask_type='MODEL_COLLISION'
|
||||
)
|
||||
collision_node = self._build_collision_node(node, shape_type)
|
||||
|
||||
if collision_node:
|
||||
print(f"成功为节点 {node.getName()} 添加 {shape_name} 碰撞体")
|
||||
@ -297,6 +307,56 @@ class PropertyHelpers:
|
||||
print(f"移除碰撞体失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _build_collision_node(self, node, shape_type, position_offset=None, visible=None, **shape_kwargs):
|
||||
"""Create a collision node for the given shape and restore editor state."""
|
||||
try:
|
||||
if not hasattr(self, 'collision_manager') or not self.collision_manager:
|
||||
return None
|
||||
collision_node = self.collision_manager.setupAdvancedCollision(
|
||||
node,
|
||||
shape_type=shape_type,
|
||||
mask_type='MODEL_COLLISION',
|
||||
**shape_kwargs,
|
||||
)
|
||||
if not collision_node:
|
||||
return None
|
||||
|
||||
if position_offset is not None:
|
||||
try:
|
||||
collision_node.setPos(*position_offset)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if visible is not None:
|
||||
if visible:
|
||||
collision_node.show()
|
||||
else:
|
||||
collision_node.hide()
|
||||
return collision_node
|
||||
except Exception as e:
|
||||
print(f"创建碰撞节点失败: {e}")
|
||||
return None
|
||||
|
||||
def _rebuild_collision_node(self, node, shape_type, position_offset=None, visible=None, **shape_kwargs):
|
||||
"""Recreate collision geometry while preserving offset/visibility by default."""
|
||||
try:
|
||||
had_collision = self._has_collision(node)
|
||||
if position_offset is None and had_collision:
|
||||
position_offset = self._get_collision_position_offset(node)
|
||||
if visible is None and had_collision:
|
||||
visible = self._is_collision_visible(node)
|
||||
self._remove_collision_from_node(node)
|
||||
return self._build_collision_node(
|
||||
node,
|
||||
shape_type,
|
||||
position_offset=position_offset,
|
||||
visible=visible,
|
||||
**shape_kwargs,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"重建碰撞节点失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _toggle_collision_visibility(self, node):
|
||||
@ -380,17 +440,8 @@ class PropertyHelpers:
|
||||
def _update_sphere_radius(self, node, radius):
|
||||
"""更新球形半径"""
|
||||
try:
|
||||
# 重新创建碰撞体来更新参数
|
||||
if hasattr(self, 'collision_manager'):
|
||||
# 先移除旧的碰撞体
|
||||
self._remove_collision_from_node(node)
|
||||
# 重新创建带有新参数的碰撞体
|
||||
self.collision_manager.setupAdvancedCollision(
|
||||
node,
|
||||
shape_type='sphere',
|
||||
mask_type='MODEL_COLLISION',
|
||||
radius=radius
|
||||
)
|
||||
self._rebuild_collision_node(node, 'sphere', radius=radius)
|
||||
print(f"更新球形半径为: {radius}")
|
||||
except Exception as e:
|
||||
print(f"更新球形半径失败: {e}")
|
||||
@ -439,14 +490,12 @@ class PropertyHelpers:
|
||||
|
||||
# 重新创建碰撞体
|
||||
if hasattr(self, 'collision_manager'):
|
||||
self._remove_collision_from_node(node)
|
||||
self.collision_manager.setupAdvancedCollision(
|
||||
node,
|
||||
shape_type='box',
|
||||
mask_type='MODEL_COLLISION',
|
||||
self._rebuild_collision_node(
|
||||
node,
|
||||
'box',
|
||||
width=new_size[0],
|
||||
length=new_size[1],
|
||||
height=new_size[2]
|
||||
height=new_size[2],
|
||||
)
|
||||
print(f"更新盒型尺寸: {new_size}")
|
||||
except Exception as e:
|
||||
@ -480,14 +529,7 @@ class PropertyHelpers:
|
||||
|
||||
# 重新创建碰撞体
|
||||
if hasattr(self, 'collision_manager'):
|
||||
self._remove_collision_from_node(node)
|
||||
self.collision_manager.setupAdvancedCollision(
|
||||
node,
|
||||
shape_type='capsule',
|
||||
mask_type='MODEL_COLLISION',
|
||||
radius=radius,
|
||||
height=height
|
||||
)
|
||||
self._rebuild_collision_node(node, 'capsule', radius=radius, height=height)
|
||||
print(f"更新胶囊体半径为: {radius}")
|
||||
except Exception as e:
|
||||
print(f"更新胶囊体半径失败: {e}")
|
||||
@ -522,14 +564,7 @@ class PropertyHelpers:
|
||||
|
||||
# 重新创建碰撞体
|
||||
if hasattr(self, 'collision_manager'):
|
||||
self._remove_collision_from_node(node)
|
||||
self.collision_manager.setupAdvancedCollision(
|
||||
node,
|
||||
shape_type='capsule',
|
||||
mask_type='MODEL_COLLISION',
|
||||
radius=radius,
|
||||
height=height
|
||||
)
|
||||
self._rebuild_collision_node(node, 'capsule', radius=radius, height=height)
|
||||
print(f"更新胶囊体高度为: {height}")
|
||||
except Exception as e:
|
||||
print(f"更新胶囊体高度失败: {e}")
|
||||
@ -578,13 +613,7 @@ class PropertyHelpers:
|
||||
|
||||
# 重新创建碰撞体
|
||||
if hasattr(self, 'collision_manager'):
|
||||
self._remove_collision_from_node(node)
|
||||
self.collision_manager.setupAdvancedCollision(
|
||||
node,
|
||||
shape_type='plane',
|
||||
mask_type='MODEL_COLLISION',
|
||||
normal=normal_vec
|
||||
)
|
||||
self._rebuild_collision_node(node, 'plane', normal=normal_vec)
|
||||
print(f"更新平面法向量为: ({normal_vec.x:.2f}, {normal_vec.y:.2f}, {normal_vec.z:.2f})")
|
||||
except Exception as e:
|
||||
print(f"更新平面法向量失败: {e}")
|
||||
@ -602,6 +631,113 @@ class PropertyHelpers:
|
||||
except Exception as e:
|
||||
print(f"手动碰撞检测失败: {e}")
|
||||
|
||||
def _capture_collision_snapshot(self, node):
|
||||
"""Capture collision editor state for undo/redo."""
|
||||
snapshot = {
|
||||
"has_collision": False,
|
||||
"shape_name": getattr(self.app, "_selected_collision_shape", "球形 (Sphere)"),
|
||||
}
|
||||
try:
|
||||
if not node or node.isEmpty() or not self._has_collision(node):
|
||||
return snapshot
|
||||
|
||||
shape_name = self._get_current_collision_shape(node)
|
||||
shape_type = self._get_current_collision_shape_type(node)
|
||||
snapshot.update({
|
||||
"has_collision": True,
|
||||
"shape_name": shape_name,
|
||||
"shape_type": shape_type,
|
||||
"position_offset": tuple(float(v) for v in self._get_collision_position_offset(node)),
|
||||
"visible": bool(self._is_collision_visible(node)),
|
||||
})
|
||||
|
||||
if shape_type == "sphere":
|
||||
snapshot["radius"] = float(self._get_sphere_radius(node))
|
||||
elif shape_type == "box":
|
||||
snapshot["size"] = tuple(float(v) for v in self._get_box_size(node))
|
||||
elif shape_type == "capsule":
|
||||
snapshot["radius"] = float(self._get_capsule_radius(node))
|
||||
snapshot["height"] = float(self._get_capsule_height(node))
|
||||
elif shape_type == "plane":
|
||||
snapshot["normal"] = tuple(float(v) for v in self._get_plane_normal(node))
|
||||
return snapshot
|
||||
except Exception as e:
|
||||
print(f"捕获碰撞快照失败: {e}")
|
||||
return snapshot
|
||||
|
||||
def _collision_snapshots_equal(self, before_snapshot, after_snapshot):
|
||||
"""Compare collision snapshots with float tolerance."""
|
||||
before_snapshot = before_snapshot or {}
|
||||
after_snapshot = after_snapshot or {}
|
||||
if bool(before_snapshot.get("has_collision")) != bool(after_snapshot.get("has_collision")):
|
||||
return False
|
||||
if before_snapshot.get("shape_name") != after_snapshot.get("shape_name"):
|
||||
return False
|
||||
if before_snapshot.get("shape_type") != after_snapshot.get("shape_type"):
|
||||
return False
|
||||
if bool(before_snapshot.get("visible", False)) != bool(after_snapshot.get("visible", False)):
|
||||
return False
|
||||
|
||||
def _rounded(values):
|
||||
if values is None:
|
||||
return None
|
||||
if isinstance(values, (tuple, list)):
|
||||
return tuple(round(float(value), 6) for value in values)
|
||||
return round(float(values), 6)
|
||||
|
||||
for key in ("position_offset", "size", "normal", "radius", "height"):
|
||||
if _rounded(before_snapshot.get(key)) != _rounded(after_snapshot.get(key)):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _apply_collision_snapshot(self, node, snapshot):
|
||||
"""Apply collision state back to a node."""
|
||||
try:
|
||||
if not node or node.isEmpty():
|
||||
return
|
||||
|
||||
snapshot = snapshot or {}
|
||||
shape_name = snapshot.get("shape_name")
|
||||
if shape_name:
|
||||
self.app._selected_collision_shape = shape_name
|
||||
|
||||
self._remove_collision_from_node(node)
|
||||
if not snapshot.get("has_collision"):
|
||||
return
|
||||
|
||||
shape_type = snapshot.get("shape_type") or self._get_shape_type_from_name(shape_name or "球形 (Sphere)")
|
||||
build_kwargs = {}
|
||||
if shape_type == "sphere":
|
||||
build_kwargs["radius"] = float(snapshot.get("radius", 1.0))
|
||||
elif shape_type == "box":
|
||||
size = snapshot.get("size") or (1.0, 1.0, 1.0)
|
||||
build_kwargs.update({
|
||||
"width": float(size[0]),
|
||||
"length": float(size[1]),
|
||||
"height": float(size[2]),
|
||||
})
|
||||
elif shape_type == "capsule":
|
||||
build_kwargs.update({
|
||||
"radius": float(snapshot.get("radius", 1.0)),
|
||||
"height": float(snapshot.get("height", 2.0)),
|
||||
})
|
||||
elif shape_type == "plane":
|
||||
from panda3d.core import Vec3
|
||||
normal = snapshot.get("normal") or (0.0, 0.0, 1.0)
|
||||
build_kwargs["normal"] = Vec3(*normal)
|
||||
|
||||
collision_node = self._build_collision_node(
|
||||
node,
|
||||
shape_type,
|
||||
position_offset=snapshot.get("position_offset"),
|
||||
visible=bool(snapshot.get("visible", False)),
|
||||
**build_kwargs,
|
||||
)
|
||||
if collision_node and snapshot.get("visible", False):
|
||||
collision_node.show()
|
||||
except Exception as e:
|
||||
print(f"应用碰撞快照失败: {e}")
|
||||
|
||||
def _update_node_name(self, node, new_name):
|
||||
"""更新节点名称"""
|
||||
if new_name and new_name != node.getName():
|
||||
@ -609,6 +745,86 @@ class PropertyHelpers:
|
||||
# 更新场景树显示
|
||||
if hasattr(self, 'scene_tree'):
|
||||
self.scene_tree.refresh()
|
||||
|
||||
def _get_light_color(self, node):
|
||||
"""Return editable light color."""
|
||||
try:
|
||||
stored_color = node.getPythonTag("editor_light_color") if hasattr(node, "getPythonTag") else None
|
||||
if stored_color and len(stored_color) >= 3:
|
||||
return (
|
||||
float(stored_color[0]),
|
||||
float(stored_color[1]),
|
||||
float(stored_color[2]),
|
||||
float(stored_color[3]) if len(stored_color) > 3 else 1.0,
|
||||
)
|
||||
|
||||
if hasattr(node, "hasColor") and node.hasColor():
|
||||
color = node.getColor()
|
||||
if color and len(color) >= 3:
|
||||
return (
|
||||
float(color[0]),
|
||||
float(color[1]),
|
||||
float(color[2]),
|
||||
float(color[3]) if len(color) > 3 else 1.0,
|
||||
)
|
||||
|
||||
rp_light = node.getPythonTag("rp_light_object")
|
||||
if rp_light is not None:
|
||||
try:
|
||||
light_color = rp_light.get_color() if hasattr(rp_light, "get_color") else getattr(rp_light, "color", None)
|
||||
if light_color is not None:
|
||||
return (
|
||||
float(light_color.x),
|
||||
float(light_color.y),
|
||||
float(light_color.z),
|
||||
1.0,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
return (1.0, 1.0, 1.0, 1.0)
|
||||
|
||||
def _apply_light_color(self, node, color):
|
||||
"""Apply light color to both editor node and RP light object."""
|
||||
try:
|
||||
rgba = (
|
||||
float(color[0]),
|
||||
float(color[1]),
|
||||
float(color[2]),
|
||||
float(color[3]) if len(color) > 3 else 1.0,
|
||||
)
|
||||
node.setPythonTag("editor_light_color", rgba)
|
||||
node.setColor(*rgba)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
rp_light = node.getPythonTag("rp_light_object")
|
||||
if rp_light is not None:
|
||||
rgb = rgba[:3]
|
||||
if hasattr(rp_light, "set_color"):
|
||||
rp_light.set_color(*rgb)
|
||||
elif hasattr(rp_light, "setColor"):
|
||||
rp_light.setColor(*rgb)
|
||||
elif hasattr(rp_light, "color"):
|
||||
rp_light.color = rgb
|
||||
if hasattr(rp_light, "set_needs_update"):
|
||||
rp_light.set_needs_update(True)
|
||||
except Exception as e:
|
||||
print(f"应用光源颜色失败: {e}")
|
||||
|
||||
def _capture_light_snapshot(self, node):
|
||||
return {"color": tuple(float(v) for v in self._get_light_color(node))}
|
||||
|
||||
def _light_snapshots_equal(self, before_snapshot, after_snapshot):
|
||||
before_color = tuple(round(float(v), 6) for v in (before_snapshot or {}).get("color", (1.0, 1.0, 1.0, 1.0)))
|
||||
after_color = tuple(round(float(v), 6) for v in (after_snapshot or {}).get("color", (1.0, 1.0, 1.0, 1.0)))
|
||||
return before_color == after_color
|
||||
|
||||
def _apply_light_snapshot(self, node, snapshot):
|
||||
color = (snapshot or {}).get("color")
|
||||
if color is not None:
|
||||
self._apply_light_color(node, color)
|
||||
|
||||
|
||||
def _get_material_base_color(self, material):
|
||||
@ -627,6 +843,118 @@ class PropertyHelpers:
|
||||
except:
|
||||
return (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||||
|
||||
def _capture_node_material_snapshot(self, node):
|
||||
"""Capture editable material state for a node."""
|
||||
materials = self._get_node_materials(node)
|
||||
if not materials:
|
||||
fallback_material = self._ensure_material_for_node(node)
|
||||
materials = [fallback_material] if fallback_material else []
|
||||
|
||||
snapshot = []
|
||||
for material in materials:
|
||||
emission = None
|
||||
try:
|
||||
if hasattr(material, "emission") and material.emission is not None:
|
||||
emission = (
|
||||
float(material.emission.x),
|
||||
float(material.emission.y),
|
||||
float(material.emission.z),
|
||||
float(material.emission.w),
|
||||
)
|
||||
except Exception:
|
||||
emission = None
|
||||
|
||||
entry = {
|
||||
"material": material,
|
||||
"base_color": tuple(float(v) for v in self._get_material_base_color(material)),
|
||||
"roughness": float(material.roughness) if hasattr(material, "roughness") and material.roughness is not None else None,
|
||||
"metallic": float(material.metallic) if hasattr(material, "metallic") and material.metallic is not None else None,
|
||||
"ior": float(material.refractive_index) if hasattr(material, "refractive_index") and material.refractive_index is not None else None,
|
||||
"emission": emission,
|
||||
}
|
||||
snapshot.append(entry)
|
||||
return snapshot
|
||||
|
||||
def _material_snapshots_equal(self, before_snapshot, after_snapshot):
|
||||
"""Compare two material snapshots with a small float tolerance."""
|
||||
before_snapshot = before_snapshot or []
|
||||
after_snapshot = after_snapshot or []
|
||||
if len(before_snapshot) != len(after_snapshot):
|
||||
return False
|
||||
|
||||
def _rounded_tuple(values):
|
||||
if values is None:
|
||||
return None
|
||||
return tuple(round(float(value), 6) for value in values)
|
||||
|
||||
for before_entry, after_entry in zip(before_snapshot, after_snapshot):
|
||||
before_material = before_entry.get("material")
|
||||
after_material = after_entry.get("material")
|
||||
before_key = getattr(before_material, "this", None) or id(before_material)
|
||||
after_key = getattr(after_material, "this", None) or id(after_material)
|
||||
if before_key != after_key:
|
||||
return False
|
||||
|
||||
if _rounded_tuple(before_entry.get("base_color")) != _rounded_tuple(after_entry.get("base_color")):
|
||||
return False
|
||||
if _rounded_tuple(before_entry.get("emission")) != _rounded_tuple(after_entry.get("emission")):
|
||||
return False
|
||||
|
||||
for field_name in ("roughness", "metallic", "ior"):
|
||||
before_value = before_entry.get(field_name)
|
||||
after_value = after_entry.get(field_name)
|
||||
if before_value is None or after_value is None:
|
||||
if before_value != after_value:
|
||||
return False
|
||||
continue
|
||||
if round(float(before_value), 6) != round(float(after_value), 6):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _apply_node_material_snapshot(self, node, snapshot):
|
||||
"""Apply a captured material snapshot back onto a node."""
|
||||
try:
|
||||
from panda3d.core import Vec4
|
||||
|
||||
if not node or node.isEmpty():
|
||||
return
|
||||
|
||||
snapshot = snapshot or []
|
||||
for entry in snapshot:
|
||||
material = entry.get("material")
|
||||
if material is None:
|
||||
continue
|
||||
|
||||
base_color = entry.get("base_color")
|
||||
if base_color is not None:
|
||||
self._set_material_base_color(material, tuple(base_color))
|
||||
|
||||
roughness = entry.get("roughness")
|
||||
if roughness is not None and hasattr(material, "set_roughness"):
|
||||
material.set_roughness(float(roughness))
|
||||
|
||||
metallic = entry.get("metallic")
|
||||
if metallic is not None and hasattr(material, "set_metallic"):
|
||||
material.set_metallic(float(metallic))
|
||||
|
||||
ior = entry.get("ior")
|
||||
if ior is not None and hasattr(material, "set_refractive_index"):
|
||||
material.set_refractive_index(float(ior))
|
||||
|
||||
emission = entry.get("emission")
|
||||
if emission is not None and hasattr(material, "set_emission"):
|
||||
material.set_emission(Vec4(*emission))
|
||||
|
||||
self._apply_material_to_geom_states(node, material)
|
||||
self._apply_material_surface_state(node, material)
|
||||
|
||||
for entry in snapshot:
|
||||
material = entry.get("material")
|
||||
if material is not None:
|
||||
self._refresh_pipeline_material_mode(node, material)
|
||||
except Exception as e:
|
||||
print(f"应用材质快照失败: {e}")
|
||||
|
||||
def _apply_material_to_geom_states(self, node, material):
|
||||
"""Bake the editable material into every GeomState so RP can see runtime edits."""
|
||||
try:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user