Fix transparency visibility issue
11
.codex/environments/environment.toml
Normal file
@ -0,0 +1,11 @@
|
||||
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
|
||||
version = 1
|
||||
name = "EG"
|
||||
|
||||
[setup]
|
||||
script = ""
|
||||
|
||||
[[actions]]
|
||||
name = "运行"
|
||||
icon = "run"
|
||||
command = "python ./main.py"
|
||||
BIN
__codex_opaque_check.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_overlay_forward_alpha_mask.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_overlay_forwardcolor_raw.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_overlay_forwardcolor_rgb.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_overlay_forwardcolor_rgb_flip.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_overlay_forwardcolor_rgb_rebind.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_overlay_red_test.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
__codex_transparency_after_material_state.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_transparency_after_split.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_transparency_bright_red.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_transparency_check.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
__codex_transparency_manual_prepare.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
__codex_transparency_overlay_flip_test.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
53
config/default_imgui_layout.ini
Normal file
@ -0,0 +1,53 @@
|
||||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,20
|
||||
Size=2048,1084
|
||||
Collapsed=0
|
||||
|
||||
[Window][工具栏]
|
||||
Pos=453,20
|
||||
Size=1326,32
|
||||
Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][场景树]
|
||||
Pos=0,20
|
||||
Size=451,748
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
|
||||
[Window][属性面板]
|
||||
Pos=1781,20
|
||||
Size=267,390
|
||||
Collapsed=0
|
||||
DockId=0x00000003,0
|
||||
|
||||
[Window][脚本管理]
|
||||
Pos=1781,412
|
||||
Size=267,356
|
||||
Collapsed=0
|
||||
DockId=0x00000004,0
|
||||
|
||||
[Window][资源管理器]
|
||||
Pos=0,770
|
||||
Size=2048,334
|
||||
Collapsed=0
|
||||
DockId=0x0000000A,0
|
||||
|
||||
[Window][控制台]
|
||||
Pos=0,770
|
||||
Size=2048,334
|
||||
Collapsed=0
|
||||
DockId=0x0000000A,1
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=Y
|
||||
DockNode ID=0x00000009 Parent=0x08BD597D SizeRef=2560,748 Split=X
|
||||
DockNode ID=0x00000007 Parent=0x00000009 SizeRef=451,1084 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=1595,1084 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1651,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=267,989 Split=Y Selected=0x3188AB8D
|
||||
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x3188AB8D
|
||||
DockNode ID=0x0000000A Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3
|
||||
130
core/render_pipeline_tag_state.py
Normal file
@ -0,0 +1,130 @@
|
||||
"""RenderPipeline tag state helpers for the editor runtime.
|
||||
|
||||
The stock native TagStateManager used by RenderPipeline creates pass-specific
|
||||
tag states from an empty RenderState. That works for static effects, but it
|
||||
drops material attributes when we edit materials live in the editor. For
|
||||
transparent forward materials this means the forward pass can no longer see
|
||||
the current MaterialAttrib.
|
||||
|
||||
This module installs a Python-side tag state manager that preserves the
|
||||
NodePath state when building tag states, and re-registers the already-created
|
||||
pipeline cameras.
|
||||
"""
|
||||
|
||||
from panda3d.core import BitMask32, ColorWriteAttrib, RenderState, ShaderAttrib
|
||||
|
||||
|
||||
class EditorTagStateManager:
|
||||
"""Tag-state manager that preserves NodePath state when applying effects."""
|
||||
|
||||
class StateContainer:
|
||||
def __init__(self, tag_name, mask, write_color):
|
||||
self.cameras = []
|
||||
self.tag_states = {}
|
||||
self.tag_name = tag_name
|
||||
self.mask = BitMask32.bit(mask)
|
||||
self.write_color = write_color
|
||||
|
||||
def __init__(self, main_cam_node):
|
||||
self._main_cam_node = main_cam_node
|
||||
self._main_cam_node.node().set_camera_mask(BitMask32.bit(1))
|
||||
self.containers = {
|
||||
"shadow": self.StateContainer("Shadows", 2, False),
|
||||
"voxelize": self.StateContainer("Voxelize", 3, False),
|
||||
"envmap": self.StateContainer("Envmap", 4, True),
|
||||
"forward": self.StateContainer("Forward", 5, True),
|
||||
}
|
||||
|
||||
def get_mask(self, container_name):
|
||||
if container_name == "gbuffer":
|
||||
return BitMask32.bit(1)
|
||||
return self.containers[container_name].mask
|
||||
|
||||
def apply_state(self, container_name, np, shader, name, sort):
|
||||
container = self.containers[container_name]
|
||||
state = np.get_state()
|
||||
|
||||
if not container.write_color:
|
||||
state = state.set_attrib(
|
||||
ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000
|
||||
)
|
||||
|
||||
state = state.set_attrib(ShaderAttrib.make(shader, sort), sort)
|
||||
container.tag_states[name] = state
|
||||
np.set_tag(container.tag_name, name)
|
||||
|
||||
for camera in container.cameras:
|
||||
camera.set_tag_state(name, state)
|
||||
|
||||
def cleanup_states(self):
|
||||
self._main_cam_node.node().clear_tag_states()
|
||||
for container in self.containers.values():
|
||||
for camera in container.cameras:
|
||||
camera.clear_tag_states()
|
||||
container.tag_states = {}
|
||||
|
||||
def register_camera(self, container_name, source):
|
||||
container = self.containers[container_name]
|
||||
source.set_tag_state_key(container.tag_name)
|
||||
source.set_camera_mask(container.mask)
|
||||
state = RenderState.make_empty()
|
||||
if not container.write_color:
|
||||
state = state.set_attrib(
|
||||
ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000
|
||||
)
|
||||
source.set_initial_state(state)
|
||||
container.cameras.append(source)
|
||||
|
||||
def unregister_camera(self, container_name, source):
|
||||
container = self.containers[container_name]
|
||||
if source not in container.cameras:
|
||||
return
|
||||
container.cameras.remove(source)
|
||||
source.clear_tag_states()
|
||||
source.set_initial_state(RenderState.make_empty())
|
||||
|
||||
|
||||
def install_editor_tag_state_manager(render_pipeline, base):
|
||||
"""Replace the native tag manager with an editor-safe Python variant."""
|
||||
if not render_pipeline or not base:
|
||||
return None
|
||||
|
||||
if isinstance(render_pipeline.tag_mgr, EditorTagStateManager):
|
||||
return render_pipeline.tag_mgr
|
||||
|
||||
try:
|
||||
render_pipeline.tag_mgr.cleanup_states()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
tag_mgr = EditorTagStateManager(base.cam)
|
||||
|
||||
for stage in render_pipeline.stage_mgr.stages:
|
||||
stage_name = type(stage).__name__
|
||||
|
||||
if hasattr(stage, "forward_cam"):
|
||||
tag_mgr.register_camera("forward", stage.forward_cam)
|
||||
|
||||
if stage_name == "EnvironmentCaptureStage" and hasattr(stage, "cameras"):
|
||||
for camera_np in stage.cameras:
|
||||
tag_mgr.register_camera("envmap", camera_np.node())
|
||||
|
||||
if hasattr(stage, "voxel_cam"):
|
||||
tag_mgr.register_camera("voxelize", stage.voxel_cam)
|
||||
|
||||
if stage_name in (
|
||||
"PSSMDistShadowStage",
|
||||
"PSSMSceneShadowStage",
|
||||
"SkyAOCaptureStage",
|
||||
) and hasattr(stage, "camera"):
|
||||
tag_mgr.register_camera("shadow", stage.camera)
|
||||
|
||||
pssm_plugin = render_pipeline.plugin_mgr.instances.get("pssm")
|
||||
if pssm_plugin and hasattr(pssm_plugin, "camera_rig"):
|
||||
split_count = pssm_plugin.get_setting("split_count")
|
||||
for index in range(split_count):
|
||||
camera_np = pssm_plugin.camera_rig.get_camera(index)
|
||||
tag_mgr.register_camera("shadow", camera_np.node())
|
||||
|
||||
render_pipeline.tag_mgr = tag_mgr
|
||||
return tag_mgr
|
||||
@ -9,8 +9,8 @@ from direct.actor.Actor import Actor
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
|
||||
from panda3d.core import (CardMaker, Vec4, Vec3, AmbientLight, DirectionalLight,
|
||||
Point3, WindowProperties, Material, LColor, loadPrcFileData,
|
||||
GraphicsPipeSelection)
|
||||
Point3, WindowProperties, Material, LColor, Shader,
|
||||
TransparencyAttrib, loadPrcFileData, GraphicsPipeSelection)
|
||||
from direct.showbase.ShowBase import ShowBase
|
||||
from direct.showbase.ShowBaseGlobal import globalClock
|
||||
from scene.scene_manager import SceneManager
|
||||
@ -25,6 +25,7 @@ from ssbo_component.ssbo_editor import SSBOEditor
|
||||
|
||||
# 从渲染管线工具模块导入全局函数
|
||||
from core.render_pipeline_utils import get_render_pipeline, set_render_pipeline
|
||||
from core.render_pipeline_tag_state import install_editor_tag_state_manager
|
||||
|
||||
# 尝试导入插件管理器(如果存在)
|
||||
try:
|
||||
@ -82,6 +83,8 @@ class CoreWorld(ShowBase):
|
||||
|
||||
# 创建渲染管线
|
||||
self.render_pipeline.create(self)
|
||||
install_editor_tag_state_manager(self.render_pipeline, self)
|
||||
self._setupForwardTransparencyOverlay()
|
||||
set_render_pipeline(self.render_pipeline)
|
||||
|
||||
# 设置相机
|
||||
@ -171,6 +174,87 @@ class CoreWorld(ShowBase):
|
||||
except Exception as e:
|
||||
print(f"清理缓存时出错: {e}")
|
||||
|
||||
def _setupForwardTransparencyOverlay(self):
|
||||
"""Composite the forward transparency result back onto the final frame.
|
||||
|
||||
The bundled RP forward merge stage is unreliable in this editor build
|
||||
during live material edits, but the forward color buffer itself is
|
||||
correct. We therefore blend that buffer as a fullscreen overlay and use
|
||||
the scene/forward depth textures to keep transparent objects behind
|
||||
opaque geometry clipped.
|
||||
"""
|
||||
self.forward_transparency_overlay = None
|
||||
|
||||
try:
|
||||
forward_stage = next(
|
||||
(
|
||||
stage for stage in self.render_pipeline.stage_mgr.stages
|
||||
if type(stage).__name__ == "ForwardStage"
|
||||
),
|
||||
None,
|
||||
)
|
||||
gbuffer_stage = next(
|
||||
(
|
||||
stage for stage in self.render_pipeline.stage_mgr.stages
|
||||
if type(stage).__name__ == "GBufferStage"
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not forward_stage or not gbuffer_stage:
|
||||
return
|
||||
|
||||
vert = """
|
||||
#version 330
|
||||
uniform mat4 p3d_ModelViewProjectionMatrix;
|
||||
in vec4 p3d_Vertex;
|
||||
in vec2 p3d_MultiTexCoord0;
|
||||
out vec2 texcoord;
|
||||
void main() {
|
||||
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
|
||||
texcoord = p3d_MultiTexCoord0;
|
||||
}
|
||||
"""
|
||||
frag = """
|
||||
#version 330
|
||||
uniform sampler2D ForwardColor;
|
||||
uniform sampler2D ForwardDepth;
|
||||
uniform sampler2D SceneDepth;
|
||||
in vec2 texcoord;
|
||||
out vec4 o_color;
|
||||
void main() {
|
||||
vec4 forward_color = texture(ForwardColor, texcoord);
|
||||
float alpha = clamp(forward_color.a, 0.0, 1.0);
|
||||
if (alpha <= 1e-4) {
|
||||
discard;
|
||||
}
|
||||
|
||||
float forward_depth = texture(ForwardDepth, texcoord).x;
|
||||
float scene_depth = texture(SceneDepth, texcoord).x;
|
||||
if (scene_depth > 1e-6 && forward_depth > scene_depth + 1e-5) {
|
||||
discard;
|
||||
}
|
||||
|
||||
o_color = vec4(forward_color.rgb, alpha);
|
||||
}
|
||||
"""
|
||||
|
||||
cm = CardMaker("ForwardTransparencyOverlay")
|
||||
cm.setFrameFullscreenQuad()
|
||||
overlay = self.render2d.attachNewNode(cm.generate())
|
||||
overlay.setShader(Shader.make(Shader.SL_GLSL, vert, frag))
|
||||
overlay.setShaderInput("ForwardColor", forward_stage.target.color_tex)
|
||||
overlay.setShaderInput("ForwardDepth", forward_stage.target.depth_tex)
|
||||
overlay.setShaderInput("SceneDepth", gbuffer_stage.target.depth_tex)
|
||||
overlay.setTransparency(TransparencyAttrib.M_alpha)
|
||||
overlay.setDepthTest(False)
|
||||
overlay.setDepthWrite(False)
|
||||
overlay.setBin("fixed", 1)
|
||||
self.forward_transparency_overlay = overlay
|
||||
print("✓ 前向透明叠加层初始化完成")
|
||||
except Exception as e:
|
||||
print(f"⚠ 初始化前向透明叠加层失败: {e}")
|
||||
|
||||
def _setupResourcePaths(self):
|
||||
"""设置Panda3D资源搜索路径,确保能正确找到Resources文件夹中的模型和贴图"""
|
||||
try:
|
||||
|
||||
62
imgui.ini
@ -25,33 +25,33 @@ Collapsed=0
|
||||
|
||||
[Window][工具栏]
|
||||
Pos=453,20
|
||||
Size=1326,32
|
||||
Size=1250,32
|
||||
Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][场景树]
|
||||
Pos=0,20
|
||||
Size=451,748
|
||||
Size=451,1036
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
|
||||
[Window][属性面板]
|
||||
Pos=1781,20
|
||||
Size=267,748
|
||||
Pos=1705,20
|
||||
Size=855,1036
|
||||
Collapsed=0
|
||||
DockId=0x00000003,0
|
||||
DockId=0x00000002,0
|
||||
|
||||
[Window][控制台]
|
||||
Pos=0,770
|
||||
Size=2048,334
|
||||
Pos=1705,20
|
||||
Size=855,1036
|
||||
Collapsed=0
|
||||
DockId=0x0000000A,1
|
||||
DockId=0x00000002,1
|
||||
|
||||
[Window][脚本管理]
|
||||
Pos=1653,20
|
||||
Size=267,390
|
||||
Pos=1950,20
|
||||
Size=610,995
|
||||
Collapsed=0
|
||||
DockId=0x00000003,1
|
||||
DockId=0x00000002,2
|
||||
|
||||
[Window][中文显示测试]
|
||||
Pos=60,60
|
||||
@ -60,7 +60,7 @@ Collapsed=0
|
||||
|
||||
[Window][WindowOverViewport_11111111]
|
||||
Pos=0,20
|
||||
Size=2048,1084
|
||||
Size=2560,1372
|
||||
Collapsed=0
|
||||
|
||||
[Window][测试窗口1]
|
||||
@ -79,7 +79,7 @@ Size=93,65
|
||||
Collapsed=0
|
||||
|
||||
[Window][新建项目]
|
||||
Pos=760,354
|
||||
Pos=824,401
|
||||
Size=400,300
|
||||
Collapsed=0
|
||||
|
||||
@ -99,10 +99,10 @@ Size=600,500
|
||||
Collapsed=0
|
||||
|
||||
[Window][资源管理器]
|
||||
Pos=0,770
|
||||
Size=2048,334
|
||||
Pos=0,1058
|
||||
Size=2560,334
|
||||
Collapsed=0
|
||||
DockId=0x0000000A,0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][创建3D文本]
|
||||
Pos=60,60
|
||||
@ -135,7 +135,7 @@ Size=89,250
|
||||
Collapsed=0
|
||||
|
||||
[Window][颜色选择器]
|
||||
Pos=810,304
|
||||
Pos=874,352
|
||||
Size=300,400
|
||||
Collapsed=0
|
||||
|
||||
@ -150,10 +150,10 @@ Size=101,226
|
||||
Collapsed=0
|
||||
|
||||
[Window][LUI编辑器]
|
||||
Pos=1113,310
|
||||
Size=267,440
|
||||
Pos=1193,20
|
||||
Size=855,748
|
||||
Collapsed=0
|
||||
DockId=0x00000004,0
|
||||
DockId=0x00000002,2
|
||||
|
||||
[Window][LUI测试控制面板]
|
||||
Pos=6,10
|
||||
@ -200,16 +200,20 @@ Pos=660,304
|
||||
Size=600,400
|
||||
Collapsed=0
|
||||
|
||||
[Window][Web面板]
|
||||
Pos=1438,20
|
||||
Size=610,748
|
||||
Collapsed=0
|
||||
DockId=0x00000002,2
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2048,1084 Split=Y
|
||||
DockNode ID=0x00000009 Parent=0x08BD597D SizeRef=2560,748 Split=X
|
||||
DockNode ID=0x00000007 Parent=0x00000009 SizeRef=451,1084 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=1595,1084 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1651,989 Split=Y
|
||||
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=2560,1372 Split=Y
|
||||
DockNode ID=0x00000005 Parent=0x08BD597D SizeRef=2560,995 Split=X
|
||||
DockNode ID=0x00000007 Parent=0x00000005 SizeRef=451,1084 Selected=0xE0015051
|
||||
DockNode ID=0x00000008 Parent=0x00000005 SizeRef=1595,1084 Split=X
|
||||
DockNode ID=0x00000001 Parent=0x00000008 SizeRef=1250,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=267,989 Split=Y Selected=0x3188AB8D
|
||||
DockNode ID=0x00000003 Parent=0x00000002 SizeRef=351,390 Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000004 Parent=0x00000002 SizeRef=351,597 Selected=0x1EB923B7
|
||||
DockNode ID=0x0000000A Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3
|
||||
DockNode ID=0x00000002 Parent=0x00000008 SizeRef=855,989 Selected=0x5DB6FF37
|
||||
DockNode ID=0x00000006 Parent=0x08BD597D SizeRef=2560,334 Selected=0x3A2E05C3
|
||||
|
||||
|
||||
154
main.py
@ -115,6 +115,17 @@ except:
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
|
||||
class MyWorld(PanelDelegates, CoreWorld):
|
||||
PANEL_VISIBILITY_ATTRS = {
|
||||
"scene_tree": "showSceneTree",
|
||||
"property": "showPropertyPanel",
|
||||
"console": "showConsole",
|
||||
"script": "showScriptPanel",
|
||||
"toolbar": "showToolbar",
|
||||
"resources": "showResourceManager",
|
||||
"web": "showWebPanel",
|
||||
"lui_editor": "showLUIEditor",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._shutdown_in_progress = False
|
||||
@ -243,6 +254,9 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
wantExplorerManager=False,
|
||||
wantTimeSliderManager=False,
|
||||
)
|
||||
self._imgui_ini_path = PROJECT_ROOT / "imgui.ini"
|
||||
self._imgui_default_layout_path = PROJECT_ROOT / "config" / "default_imgui_layout.ini"
|
||||
imgui.get_io().set_ini_filename(self._imgui_ini_path.as_posix())
|
||||
# Let ssbo_component reuse the existing imgui backend instance.
|
||||
self.imgui_backend = self.imgui
|
||||
# Initialize SSBO editor and let it own mouse1 picking.
|
||||
@ -274,6 +288,7 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self.animation_tools = AnimationTools(self)
|
||||
self.property_helpers = PropertyHelpers(self)
|
||||
self.app_actions = AppActions(self)
|
||||
self._ensure_default_imgui_layout()
|
||||
|
||||
# 简化的初始化字体设置(只使用中文字体)
|
||||
try:
|
||||
@ -320,13 +335,19 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self.showDemoWindow = False
|
||||
|
||||
# UI状态管理
|
||||
self.showSceneTree = True
|
||||
self.showPropertyPanel = True
|
||||
self.showConsole = True
|
||||
self.showScriptPanel = not self.use_ssbo_mouse_picking
|
||||
self.showToolbar = True
|
||||
self.showResourceManager = True
|
||||
self.showWebPanel = False
|
||||
self._default_panel_visibility = {
|
||||
"scene_tree": True,
|
||||
"property": True,
|
||||
"console": True,
|
||||
"script": not self.use_ssbo_mouse_picking,
|
||||
"toolbar": True,
|
||||
"resources": True,
|
||||
"web": False,
|
||||
"lui_editor": not self.use_ssbo_mouse_picking,
|
||||
}
|
||||
for attr_name in self.PANEL_VISIBILITY_ATTRS.values():
|
||||
setattr(self, attr_name, False)
|
||||
self.reset_panel_visibility_to_defaults()
|
||||
self.webPanelUrl = "https://www.baidu.com"
|
||||
|
||||
# 脚本系统状态变量
|
||||
@ -334,6 +355,7 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self._new_script_name = "new_script"
|
||||
self._selected_template = 0
|
||||
self._mount_script_index = 0
|
||||
self.console_command_input = ""
|
||||
|
||||
# 变换监控相关
|
||||
self._transform_monitoring = False
|
||||
@ -342,6 +364,8 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self._transform_update_timer = 0
|
||||
self._transform_update_interval = 0.05 # 50ms检查一次
|
||||
self._clipboard_pos = None # 位置剪贴板
|
||||
self._transform_clipboard = {}
|
||||
self._transform_scale_locked = True
|
||||
|
||||
# 颜色选择器相关
|
||||
self._color_picker_active = False
|
||||
@ -353,6 +377,7 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self._font_selector_active = False
|
||||
self._font_selector_target = None # (target_object, property_name)
|
||||
self._font_selector_current_font = ""
|
||||
self._font_selector_search_text = ""
|
||||
self._font_selector_callback = None
|
||||
self._available_fonts = [] # 可用字体列表
|
||||
self._refresh_available_fonts()
|
||||
@ -407,8 +432,6 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self._toolbar_window_rect = None
|
||||
self._drag_scene_tree_hover_node = None
|
||||
self.model_drag_drop = ModelDragDropService(self)
|
||||
self.showLUIEditor = not self.use_ssbo_mouse_picking
|
||||
|
||||
# 导入功能状态
|
||||
self.show_import_dialog = False
|
||||
self.import_file_path = ""
|
||||
@ -498,6 +521,103 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
|
||||
print("✓ MyWorld 初始化完成")
|
||||
|
||||
def is_panel_visible(self, panel_key):
|
||||
attr_name = self.PANEL_VISIBILITY_ATTRS.get(panel_key)
|
||||
if not attr_name:
|
||||
return False
|
||||
return bool(getattr(self, attr_name, False))
|
||||
|
||||
def set_panel_visible(self, panel_key, visible):
|
||||
attr_name = self.PANEL_VISIBILITY_ATTRS.get(panel_key)
|
||||
if not attr_name:
|
||||
return
|
||||
|
||||
visible = bool(visible)
|
||||
previous = bool(getattr(self, attr_name, False))
|
||||
setattr(self, attr_name, visible)
|
||||
|
||||
if panel_key == "web" and previous != visible:
|
||||
if not visible:
|
||||
try:
|
||||
self.editor_panels._stop_imgui_webview()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if panel_key == "lui_editor" and hasattr(self, "lui_manager"):
|
||||
try:
|
||||
self.lui_manager.show_editor = visible
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def reset_panel_visibility_to_defaults(self):
|
||||
for panel_key, default_visible in self._default_panel_visibility.items():
|
||||
self.set_panel_visible(panel_key, default_visible)
|
||||
|
||||
def _imgui_layout_looks_valid(self, ini_text):
|
||||
"""Conservatively check whether imgui.ini already contains a usable dock layout."""
|
||||
if not ini_text or "[Docking][Data]" not in ini_text or "DockSpace" not in ini_text:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _ensure_default_imgui_layout(self):
|
||||
"""Seed a default dock layout when imgui.ini is missing or obviously incomplete."""
|
||||
template_path = getattr(self, "_imgui_default_layout_path", None)
|
||||
ini_path = getattr(self, "_imgui_ini_path", None)
|
||||
if not template_path or not ini_path or not template_path.exists():
|
||||
return
|
||||
|
||||
current_text = ""
|
||||
if ini_path.exists():
|
||||
try:
|
||||
current_text = ini_path.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
current_text = ini_path.read_text(encoding="utf-8", errors="ignore")
|
||||
except Exception as e:
|
||||
print(f"⚠ 读取 ImGui 布局文件失败: {e}")
|
||||
|
||||
if self._imgui_layout_looks_valid(current_text):
|
||||
return
|
||||
|
||||
try:
|
||||
template_text = template_path.read_text(encoding="utf-8")
|
||||
except Exception as e:
|
||||
print(f"⚠ 读取默认 ImGui 布局模板失败: {e}")
|
||||
return
|
||||
|
||||
if not self._imgui_layout_looks_valid(template_text):
|
||||
print("⚠ 默认 ImGui 布局模板不完整,跳过自动回填")
|
||||
return
|
||||
|
||||
try:
|
||||
if ini_path.exists() and current_text.strip():
|
||||
backup_path = ini_path.with_suffix(".invalid.bak")
|
||||
if not backup_path.exists():
|
||||
shutil.copyfile(ini_path, backup_path)
|
||||
print(f"ℹ 已备份无效 ImGui 布局: {backup_path}")
|
||||
|
||||
imgui.load_ini_settings_from_memory(template_text)
|
||||
ini_path.write_text(template_text, encoding="utf-8")
|
||||
print(f"✓ 已加载默认 ImGui 布局: {template_path}")
|
||||
except Exception as e:
|
||||
print(f"⚠ 应用默认 ImGui 布局失败: {e}")
|
||||
|
||||
def _reset_imgui_layout(self):
|
||||
"""Force-reset the current ImGui dock layout from the default template."""
|
||||
template_path = getattr(self, "_imgui_default_layout_path", None)
|
||||
ini_path = getattr(self, "_imgui_ini_path", None)
|
||||
if not template_path or not ini_path or not template_path.exists():
|
||||
self.add_error_message("默认 ImGui 布局模板不存在")
|
||||
return
|
||||
|
||||
try:
|
||||
template_text = template_path.read_text(encoding="utf-8")
|
||||
imgui.load_ini_settings_from_memory(template_text)
|
||||
ini_path.write_text(template_text, encoding="utf-8")
|
||||
self.reset_panel_visibility_to_defaults()
|
||||
self.add_success_message("ImGui 布局已重置为默认布局")
|
||||
except Exception as e:
|
||||
self.add_error_message(f"重置 ImGui 布局失败: {e}")
|
||||
|
||||
def _on_window_event(self, window):
|
||||
"""窗口事件处理:窗口被关闭时退出应用。"""
|
||||
if self._shutdown_in_progress:
|
||||
@ -855,7 +975,7 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
self._draw_drag_drop_interface()
|
||||
|
||||
# 绘制LUI编辑器
|
||||
if self.showLUIEditor and hasattr(self, 'lui_manager'):
|
||||
if self.is_panel_visible("lui_editor") and hasattr(self, 'lui_manager'):
|
||||
self.lui_manager.draw_editor()
|
||||
|
||||
# 更新变换监控
|
||||
@ -865,31 +985,31 @@ class MyWorld(PanelDelegates, CoreWorld):
|
||||
def _draw_docked_layout(self, window_width, window_height):
|
||||
"""绘制可停靠的布局(支持拖拽)"""
|
||||
# 左侧场景树面板
|
||||
if self.showSceneTree:
|
||||
if self.is_panel_visible("scene_tree"):
|
||||
self._draw_scene_tree()
|
||||
|
||||
# 资源管理器面板
|
||||
if self.showResourceManager:
|
||||
if self.is_panel_visible("resources"):
|
||||
self._draw_resource_manager()
|
||||
|
||||
# 属性面板
|
||||
if self.showPropertyPanel:
|
||||
if self.is_panel_visible("property"):
|
||||
self._draw_property_panel()
|
||||
|
||||
# Web面板
|
||||
if self.showWebPanel:
|
||||
if self.is_panel_visible("web"):
|
||||
self._draw_web_panel()
|
||||
|
||||
# 脚本面板
|
||||
if self.showScriptPanel:
|
||||
if self.is_panel_visible("script"):
|
||||
self._draw_script_panel()
|
||||
|
||||
# 底部控制台
|
||||
if self.showConsole:
|
||||
if self.is_panel_visible("console"):
|
||||
self._draw_console()
|
||||
|
||||
# 顶部工具栏
|
||||
if self.showToolbar:
|
||||
if self.is_panel_visible("toolbar"):
|
||||
self._draw_toolbar()
|
||||
|
||||
def _sync_rp_light_from_node(self, node):
|
||||
|
||||
225
third_party/p3dimgui/backend.py
vendored
@ -36,6 +36,12 @@ import ctypes
|
||||
|
||||
import pyperclip
|
||||
import sys
|
||||
import time
|
||||
|
||||
if sys.platform == "win32":
|
||||
from ctypes import wintypes
|
||||
else:
|
||||
wintypes = None
|
||||
|
||||
__all__ = ['ImGuiBackend', 'ImGuiStyles']
|
||||
|
||||
@ -199,11 +205,21 @@ class ImGuiBackend(DirectObject):
|
||||
self.textureCounter = 0
|
||||
self.textures: dict[int, Texture] = {}
|
||||
self.geomData: list[GeomList] = []
|
||||
self._keystroke_observed = False
|
||||
self._last_synthesized_text = ""
|
||||
self._last_synthesized_text_at = 0.0
|
||||
self._ime_candidate_text = ""
|
||||
self._ime_candidate_highlight = (0, 0, 0)
|
||||
self._ime_composing = False
|
||||
self._ime_open = False
|
||||
self._last_ime_result_text = ""
|
||||
self._imm32 = None
|
||||
|
||||
self.__setupStyle(style)
|
||||
self.__setupGeom()
|
||||
self.__setupShader()
|
||||
self.__setupFront()
|
||||
self.__setupImeSupport()
|
||||
self.__setupEvent()
|
||||
self.__windowEvent()
|
||||
self.__setupButton()
|
||||
@ -246,16 +262,168 @@ class ImGuiBackend(DirectObject):
|
||||
case _:
|
||||
self.notify.warning(f"Unknown style: \"{style}\"")
|
||||
|
||||
@staticmethod
|
||||
def __modifier_prefixes():
|
||||
return (
|
||||
'control-', 'alt-', 'shift-', 'shift-control-', 'shift-alt-', 'shift-control-alt-',
|
||||
'meta-', 'control-meta-', 'alt-meta-', 'control-alt-meta-', 'shift-meta-',
|
||||
'shift-control-meta', 'shift-alt-meta-', 'shift-control-alt-meta-'
|
||||
)
|
||||
|
||||
def __strip_modifier_prefixes(self, keyName: str):
|
||||
had_shift = False
|
||||
original_key_name = keyName or ""
|
||||
if original_key_name.startswith(self.__modifier_prefixes()):
|
||||
had_shift = 'shift-' in original_key_name
|
||||
keyName = original_key_name.split('-')[-1]
|
||||
if keyName == '':
|
||||
keyName = '-'
|
||||
return keyName, had_shift
|
||||
|
||||
@staticmethod
|
||||
def __apply_shift_to_ascii(character: str):
|
||||
if not character:
|
||||
return ""
|
||||
if character.isalpha():
|
||||
return character.upper()
|
||||
|
||||
shift_map = {
|
||||
"1": "!",
|
||||
"2": "@",
|
||||
"3": "#",
|
||||
"4": "$",
|
||||
"5": "%",
|
||||
"6": "^",
|
||||
"7": "&",
|
||||
"8": "*",
|
||||
"9": "(",
|
||||
"0": ")",
|
||||
"-": "_",
|
||||
"=": "+",
|
||||
"[": "{",
|
||||
"]": "}",
|
||||
"\\": "|",
|
||||
";": ":",
|
||||
"'": "\"",
|
||||
",": "<",
|
||||
".": ">",
|
||||
"/": "?",
|
||||
"`": "~",
|
||||
}
|
||||
return shift_map.get(character, character)
|
||||
|
||||
def __resolve_text_input(self, keyName: str, button: ButtonHandle | None = None):
|
||||
resolved_key_name = keyName or ""
|
||||
if resolved_key_name == ' ':
|
||||
return ' '
|
||||
|
||||
if button is None:
|
||||
button = ButtonRegistry.ptr().getButton(resolved_key_name)
|
||||
if button != ButtonHandle.none() and button.hasAsciiEquivalent():
|
||||
character = button.getAsciiEquivalent()
|
||||
if character and ord(character) >= 32:
|
||||
return character
|
||||
|
||||
if resolved_key_name and any(ord(character) > 126 for character in resolved_key_name):
|
||||
if all(ord(character) >= 32 for character in resolved_key_name):
|
||||
return resolved_key_name
|
||||
return ""
|
||||
|
||||
def __queue_text_input(self, text: str):
|
||||
if not text:
|
||||
return
|
||||
self.io.add_input_characters_utf8(text)
|
||||
|
||||
def __setupImeSupport(self):
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
try:
|
||||
self._imm32 = ctypes.WinDLL("imm32", use_last_error=True)
|
||||
self._imm32.ImmGetContext.argtypes = [wintypes.HWND]
|
||||
self._imm32.ImmGetContext.restype = wintypes.HANDLE
|
||||
self._imm32.ImmReleaseContext.argtypes = [wintypes.HWND, wintypes.HANDLE]
|
||||
self._imm32.ImmReleaseContext.restype = wintypes.BOOL
|
||||
self._imm32.ImmGetOpenStatus.argtypes = [wintypes.HANDLE]
|
||||
self._imm32.ImmGetOpenStatus.restype = wintypes.BOOL
|
||||
self._imm32.ImmGetCompositionStringW.argtypes = [
|
||||
wintypes.HANDLE,
|
||||
wintypes.DWORD,
|
||||
wintypes.LPVOID,
|
||||
wintypes.DWORD,
|
||||
]
|
||||
self._imm32.ImmGetCompositionStringW.restype = ctypes.c_long
|
||||
except Exception:
|
||||
self._imm32 = None
|
||||
|
||||
def __get_window_handle(self):
|
||||
if not self.window:
|
||||
return None
|
||||
try:
|
||||
win_handle = self.window.getWindowHandle()
|
||||
if not win_handle:
|
||||
return None
|
||||
return win_handle.getIntHandle()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def __read_ime_string(self, himc, composition_type):
|
||||
if not self._imm32:
|
||||
return ""
|
||||
byte_length = self._imm32.ImmGetCompositionStringW(himc, composition_type, None, 0)
|
||||
if byte_length <= 0:
|
||||
return ""
|
||||
buffer = ctypes.create_unicode_buffer((byte_length // ctypes.sizeof(ctypes.c_wchar)) + 1)
|
||||
copied = self._imm32.ImmGetCompositionStringW(himc, composition_type, buffer, byte_length)
|
||||
if copied <= 0:
|
||||
return ""
|
||||
return buffer.value
|
||||
|
||||
def __pollWindowsIme(self):
|
||||
if sys.platform != "win32" or not self._imm32:
|
||||
return
|
||||
|
||||
hwnd = self.__get_window_handle()
|
||||
if not hwnd:
|
||||
return
|
||||
|
||||
himc = self._imm32.ImmGetContext(hwnd)
|
||||
if not himc:
|
||||
self._ime_open = False
|
||||
self._ime_composing = False
|
||||
self._last_ime_result_text = ""
|
||||
return
|
||||
|
||||
GCS_COMPSTR = 0x0008
|
||||
GCS_RESULTSTR = 0x0800
|
||||
|
||||
try:
|
||||
self._ime_open = bool(self._imm32.ImmGetOpenStatus(himc))
|
||||
composition_text = self.__read_ime_string(himc, GCS_COMPSTR)
|
||||
result_text = self.__read_ime_string(himc, GCS_RESULTSTR)
|
||||
self._ime_composing = bool(composition_text)
|
||||
|
||||
if result_text and result_text != self._last_ime_result_text:
|
||||
self.__queue_text_input(result_text)
|
||||
self._last_ime_result_text = result_text
|
||||
elif not result_text:
|
||||
self._last_ime_result_text = ""
|
||||
finally:
|
||||
self._imm32.ImmReleaseContext(hwnd, himc)
|
||||
|
||||
def __onCandidate(self, candidate_text, highlight_start = 0, highlight_end = 0, cursor_pos = 0):
|
||||
if candidate_text is None:
|
||||
candidate_text = ""
|
||||
self._ime_candidate_text = str(candidate_text)
|
||||
self._ime_candidate_highlight = (
|
||||
int(highlight_start or 0),
|
||||
int(highlight_end or 0),
|
||||
int(cursor_pos or 0),
|
||||
)
|
||||
|
||||
def __onButton(self, keyName: str, down: bool):
|
||||
# Panda3D adds the prefix of the modifier keys to the key name
|
||||
# if they are held down, so we have to strip them out.
|
||||
if keyName.startswith(('control-', 'alt-', 'shift-', 'shift-control-', 'shift-alt-', 'shift-control-alt-',
|
||||
'meta-', 'control-meta-', 'alt-meta-', 'control-alt-meta-', 'shift-meta-',
|
||||
'shift-control-meta', 'shift-alt-meta-', 'shift-control-alt-meta-')):
|
||||
keyName = keyName.split('-')[-1]
|
||||
if keyName == '':
|
||||
# must be minus.
|
||||
keyName = '-'
|
||||
keyName, had_shift = self.__strip_modifier_prefixes(keyName)
|
||||
|
||||
button = ButtonRegistry.ptr().getButton(keyName)
|
||||
if button == ButtonHandle.none():
|
||||
@ -285,6 +453,25 @@ class ImGuiBackend(DirectObject):
|
||||
imguiKey = KEYBOARD_BUTTON_TO_IMGUI_KEY.get(button, imgui.Key.none.value)
|
||||
self.io.add_key_event(imguiKey, down)
|
||||
|
||||
# Some Panda3D / Windows combinations do not emit a reliable "keystroke"
|
||||
# event for ImGui text fields. Fall back to printable key-down events
|
||||
# until we observe real text events in this session.
|
||||
if (
|
||||
down
|
||||
and not self._keystroke_observed
|
||||
and not self.io.key_ctrl
|
||||
and not self.io.key_alt
|
||||
and not self.io.key_super
|
||||
and not (sys.platform == "win32" and self._ime_open and self.io.want_text_input)
|
||||
):
|
||||
text = self.__resolve_text_input(keyName, button)
|
||||
if had_shift:
|
||||
text = self.__apply_shift_to_ascii(text)
|
||||
if text:
|
||||
self._last_synthesized_text = text
|
||||
self._last_synthesized_text_at = time.monotonic()
|
||||
self.__queue_text_input(text)
|
||||
|
||||
def __onKeystroke(self, keyName):
|
||||
# NOTE: Panda3D for some reason doesn't recognize if
|
||||
# the caps lock is on for macOS. You would have to
|
||||
@ -294,8 +481,22 @@ class ImGuiBackend(DirectObject):
|
||||
if keyName == ' ':
|
||||
# There is no space button on the ButtonRegistry.
|
||||
button = KeyboardButton.space()
|
||||
if button.hasAsciiEquivalent():
|
||||
self.io.add_input_character(ord(button.getAsciiEquivalent()))
|
||||
|
||||
text = self.__resolve_text_input(keyName, button)
|
||||
if not text:
|
||||
return
|
||||
|
||||
now = time.monotonic()
|
||||
if (
|
||||
not self._keystroke_observed
|
||||
and text == self._last_synthesized_text
|
||||
and (now - self._last_synthesized_text_at) < 0.25
|
||||
):
|
||||
self._keystroke_observed = True
|
||||
return
|
||||
|
||||
self._keystroke_observed = True
|
||||
self.__queue_text_input(text)
|
||||
|
||||
def __setupGeom(self):
|
||||
self.notify.debug("__setupGeom")
|
||||
@ -349,6 +550,7 @@ class ImGuiBackend(DirectObject):
|
||||
base.buttonThrowers[0].node().setButtonDownEvent('buttonDown')
|
||||
base.buttonThrowers[0].node().setButtonUpEvent('buttonUp')
|
||||
base.buttonThrowers[0].node().setKeystrokeEvent('keystroke')
|
||||
base.buttonThrowers[0].node().setCandidateEvent('candidate')
|
||||
|
||||
def __buttonDown(keyName):
|
||||
self.__onButton(keyName, True)
|
||||
@ -359,6 +561,9 @@ class ImGuiBackend(DirectObject):
|
||||
def __keyStroke(keyName):
|
||||
self.__onKeystroke(keyName)
|
||||
|
||||
def __candidate(candidate_text, highlight_start, highlight_end, cursor_pos):
|
||||
self.__onCandidate(candidate_text, highlight_start, highlight_end, cursor_pos)
|
||||
|
||||
def __handleOobe():
|
||||
if base.bboard.get('oobeEnabled'):
|
||||
self.ignore('buttonDown')
|
||||
@ -378,6 +583,7 @@ class ImGuiBackend(DirectObject):
|
||||
self.accept('buttonDown', __buttonDown)
|
||||
self.accept('buttonUp', __buttonUp)
|
||||
self.accept('keystroke', __keyStroke)
|
||||
self.accept('candidate', __candidate)
|
||||
|
||||
self.accept(base.bboard.getEvent('oobeEnabled'), __handleOobe)
|
||||
|
||||
@ -394,6 +600,7 @@ class ImGuiBackend(DirectObject):
|
||||
return task.cont
|
||||
|
||||
self.__refreshDisplayMetrics()
|
||||
self.__pollWindowsIme()
|
||||
self.io.delta_time = base.clock.getDt()
|
||||
if self.window:
|
||||
mouse = self.window.getPointer(0)
|
||||
|
||||
BIN
tmp_transparency_check/opaque.png
Normal file
|
After Width: | Height: | Size: 785 KiB |
BIN
tmp_transparency_check/transparent_01.png
Normal file
|
After Width: | Height: | Size: 785 KiB |
@ -621,11 +621,15 @@ class LUIManagerEditorMixin:
|
||||
return
|
||||
|
||||
imgui.set_next_window_size((350, 600), imgui.Cond_.first_use_ever)
|
||||
with imgui_ctx.begin("LUI编辑器", True) as (opened, show):
|
||||
self.show_editor = opened
|
||||
if hasattr(self, "world") and hasattr(self.world, "showLUIEditor"):
|
||||
self.world.showLUIEditor = opened
|
||||
if not show:
|
||||
with imgui_ctx.begin("LUI编辑器", self.show_editor) as window:
|
||||
self.show_editor = bool(window.opened)
|
||||
if hasattr(self, "world") and hasattr(self.world, "set_panel_visible"):
|
||||
self.world.set_panel_visible("lui_editor", window.opened)
|
||||
elif hasattr(self, "world") and hasattr(self.world, "showLUIEditor"):
|
||||
self.world.showLUIEditor = bool(window.opened)
|
||||
if not window.opened:
|
||||
return
|
||||
if not window.expanded:
|
||||
return
|
||||
|
||||
# Play/Stop
|
||||
|
||||
@ -122,7 +122,7 @@ class CreateActions:
|
||||
|
||||
def _on_open_scripts_manager(self):
|
||||
"""打开脚本管理器"""
|
||||
self.showScriptPanel = True
|
||||
self.set_panel_visible("script", True)
|
||||
self.add_info_message("脚本管理器已打开")
|
||||
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ class EditorPanelsCenterMixin:
|
||||
def _on_create_web_panel(self):
|
||||
"""创建或激活 ImGui Web 面板。"""
|
||||
self._ensure_web_panel_state()
|
||||
self.app.showWebPanel = True
|
||||
self.app.set_panel_visible("web", True)
|
||||
|
||||
webview = getattr(self.app, "_imgui_webview", None)
|
||||
if webview and getattr(webview, "_running", False):
|
||||
@ -143,18 +143,18 @@ class EditorPanelsCenterMixin:
|
||||
def _draw_web_panel(self):
|
||||
"""绘制 Web 面板(ImGui + 后台浏览器截图)。"""
|
||||
self._ensure_web_panel_state()
|
||||
if not self.app.showWebPanel:
|
||||
if not self.app.is_panel_visible("web"):
|
||||
self._stop_imgui_webview()
|
||||
return
|
||||
|
||||
flags = self.app.style_manager.get_window_flags("panel")
|
||||
with self.app.style_manager.begin_styled_window("Web面板", self.app.showWebPanel, flags) as (_, opened):
|
||||
if not opened:
|
||||
self.app.showWebPanel = False
|
||||
self.app.set_panel_visible("web", False)
|
||||
self._stop_imgui_webview()
|
||||
return
|
||||
|
||||
self.app.showWebPanel = True
|
||||
self.app.set_panel_visible("web", opened)
|
||||
|
||||
changed, self.app.web_panel_url_input = imgui.input_text(
|
||||
"URL", self.app.web_panel_url_input, 1024
|
||||
|
||||
@ -75,6 +75,14 @@ class EditorPanelsRightMixin(
|
||||
imgui.bullet_text("使用 F 键快速聚焦到选中对象")
|
||||
imgui.bullet_text("使用 Delete 键删除选中对象")
|
||||
|
||||
def _draw_property_section(self, title, draw_callback, default_open=False):
|
||||
flags = imgui.TreeNodeFlags_.span_avail_width.value
|
||||
if default_open:
|
||||
flags |= imgui.TreeNodeFlags_.default_open.value
|
||||
if imgui.collapsing_header(title, flags):
|
||||
draw_callback()
|
||||
imgui.spacing()
|
||||
|
||||
def _draw_ssbo_selection_summary(self, summary):
|
||||
"""Render a safe summary for SSBO group selections without exposing wrong node properties."""
|
||||
imgui.spacing()
|
||||
@ -109,127 +117,98 @@ class EditorPanelsRightMixin(
|
||||
# 获取节点基本信息
|
||||
node_name = node.getName() or "未命名节点"
|
||||
node_type = self.app._get_node_type_from_node(node)
|
||||
|
||||
# 添加一些间距,模仿Qt版本的布局
|
||||
self._draw_object_overview(node, node_name, node_type)
|
||||
imgui.spacing()
|
||||
|
||||
# 物体名称组(使用Qt版本的样式)
|
||||
if imgui.collapsing_header("物体名称"):
|
||||
# 第一行:可见性复选框和名称输入
|
||||
user_visible = node.getPythonTag("user_visible")
|
||||
if user_visible is None:
|
||||
user_visible = True
|
||||
node.setPythonTag("user_visible", True)
|
||||
|
||||
# 可见性复选框(模仿Qt版本的样式)
|
||||
changed, is_visible = imgui.checkbox("##visibility", user_visible)
|
||||
if changed:
|
||||
node.setPythonTag("user_visible", is_visible)
|
||||
if is_visible:
|
||||
node.show()
|
||||
else:
|
||||
node.hide()
|
||||
|
||||
imgui.same_line()
|
||||
imgui.text("可见")
|
||||
imgui.same_line()
|
||||
imgui.spacing()
|
||||
imgui.same_line()
|
||||
|
||||
# 名称输入框(模仿Qt版本的样式)
|
||||
imgui.text("名称:")
|
||||
imgui.same_line()
|
||||
changed, new_name = imgui.input_text("##name", node_name, 256)
|
||||
if changed and hasattr(self.app, 'selection'):
|
||||
# 更新场景树中的名称
|
||||
self.app._update_node_name(node, new_name)
|
||||
|
||||
# 添加分隔线
|
||||
imgui.separator()
|
||||
|
||||
# 状态徽章(模仿Qt版本的徽章样式)
|
||||
self.app._draw_status_badges(node)
|
||||
|
||||
imgui.spacing()
|
||||
|
||||
# 变换属性组
|
||||
if imgui.collapsing_header("变换 Transform"):
|
||||
self.app._draw_transform_properties(node)
|
||||
|
||||
# 根据节点类型显示特定属性组
|
||||
|
||||
self._draw_property_section("变换", lambda: self.app._draw_transform_properties(node), default_open=True)
|
||||
|
||||
if node_type == "GUI元素":
|
||||
if imgui.collapsing_header("GUI信息"):
|
||||
self.app._draw_gui_properties(node)
|
||||
self._draw_property_section("GUI", lambda: self.app._draw_gui_properties(node), default_open=True)
|
||||
elif node_type == "光源":
|
||||
if imgui.collapsing_header("光源属性"):
|
||||
self.app._draw_light_properties(node)
|
||||
self._draw_property_section("光源", lambda: self.app._draw_light_properties(node), default_open=True)
|
||||
elif node_type == "模型":
|
||||
if imgui.collapsing_header("模型属性"):
|
||||
self.app._draw_model_properties(node)
|
||||
|
||||
# 动画控制组(只对模型显示)
|
||||
if imgui.collapsing_header("动画控制"):
|
||||
self.app._draw_animation_properties(node)
|
||||
|
||||
# 外观属性组(通用)
|
||||
if imgui.collapsing_header("外观属性"):
|
||||
self.app._draw_appearance_properties(node)
|
||||
|
||||
# 碰撞检测组
|
||||
if imgui.collapsing_header("碰撞检测"):
|
||||
self.app._draw_collision_properties(node)
|
||||
|
||||
# 操作按钮组
|
||||
if imgui.collapsing_header("操作"):
|
||||
self.app._draw_property_actions(node)
|
||||
self._draw_property_section("模型", lambda: self.app._draw_model_properties(node), default_open=True)
|
||||
self._draw_property_section("动画", lambda: self.app._draw_animation_properties(node), default_open=False)
|
||||
|
||||
self._draw_property_section("材质", lambda: self.app._draw_appearance_properties(node), default_open=True)
|
||||
self._draw_property_section("碰撞", lambda: self.app._draw_collision_properties(node), default_open=False)
|
||||
self._draw_property_section("操作", lambda: self.app._draw_property_actions(node), default_open=False)
|
||||
|
||||
def _draw_object_overview(self, node, node_name, node_type):
|
||||
imgui.separator_text("对象")
|
||||
|
||||
user_visible = node.getPythonTag("user_visible")
|
||||
if user_visible is None:
|
||||
user_visible = True
|
||||
node.setPythonTag("user_visible", True)
|
||||
|
||||
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()
|
||||
|
||||
imgui.same_line()
|
||||
imgui.text_disabled("启用")
|
||||
imgui.same_line()
|
||||
imgui.set_next_item_width(-1)
|
||||
changed, new_name = imgui.input_text("##name", node_name, 256)
|
||||
if changed and hasattr(self.app, "selection"):
|
||||
self.app._update_node_name(node, new_name)
|
||||
|
||||
parent_name = "SceneRoot"
|
||||
try:
|
||||
parent = node.getParent()
|
||||
if parent and not parent.isEmpty():
|
||||
parent_name = parent.getName() or "SceneRoot"
|
||||
except Exception:
|
||||
parent = None
|
||||
|
||||
imgui.text_disabled(f"{node_type} | Parent: {parent_name}")
|
||||
|
||||
if hasattr(node, "getPythonTag") and node.getPythonTag("script"):
|
||||
imgui.same_line()
|
||||
imgui.text_colored((0.8, 0.4, 0.8, 1.0), "[Script]")
|
||||
|
||||
if node_type == "模型" and node.hasTag("has_animations") and node.getTag("has_animations").lower() == "true":
|
||||
imgui.same_line()
|
||||
imgui.text_colored((0.4, 0.8, 0.4, 1.0), "[Animation]")
|
||||
|
||||
def _draw_status_badges(self, node):
|
||||
"""绘制状态徽章(模仿Qt版本的徽章样式)"""
|
||||
imgui.text("状态标签: ")
|
||||
|
||||
# 可见性状态徽章
|
||||
"""绘制对象状态徽章行。"""
|
||||
is_visible = not node.is_hidden()
|
||||
visibility_color = (0.176, 1.0, 0.769, 1.0) if is_visible else (0.953, 0.616, 0.471, 1.0)
|
||||
visibility_text = "可见" if is_visible else "隐藏"
|
||||
|
||||
imgui.same_line()
|
||||
imgui.text_colored(visibility_color, f"[{visibility_text}]")
|
||||
|
||||
# 节点类型徽章
|
||||
visibility_text = "Visible" if is_visible else "Hidden"
|
||||
|
||||
badges = [(visibility_text, visibility_color)]
|
||||
|
||||
node_type = self._get_node_type_from_node(node)
|
||||
type_colors = {
|
||||
"GUI元素": (0.188, 0.404, 0.753, 1.0), # 主题蓝色
|
||||
"光源": (1.0, 0.8, 0.2, 1.0), # 黄色
|
||||
"模型": (0.6, 0.8, 1.0, 1.0), # 浅蓝色
|
||||
"相机": (0.8, 0.8, 0.2, 1.0), # 橙色
|
||||
"几何体": (0.5, 0.5, 0.5, 1.0), # 灰色
|
||||
"GUI元素": (0.188, 0.404, 0.753, 1.0),
|
||||
"光源": (1.0, 0.8, 0.2, 1.0),
|
||||
"模型": (0.6, 0.8, 1.0, 1.0),
|
||||
"相机": (0.8, 0.8, 0.2, 1.0),
|
||||
"几何体": (0.5, 0.5, 0.5, 1.0),
|
||||
}
|
||||
|
||||
if node_type in type_colors:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(type_colors[node_type], f"[{node_type}]")
|
||||
|
||||
# 功能性徽章
|
||||
badges = []
|
||||
|
||||
# 碰撞体徽章
|
||||
has_collision = hasattr(node, 'getChild') and any('Collision' in child.getName() for child in node.getChildren() if child.getName())
|
||||
badges.append((node_type, type_colors[node_type]))
|
||||
|
||||
has_collision = hasattr(node, "getChild") and any(
|
||||
"Collision" in child.getName() for child in node.getChildren() if child.getName()
|
||||
)
|
||||
if has_collision:
|
||||
badges.append(("碰撞", (0.2, 0.4, 0.8, 1.0))) # 蓝色
|
||||
|
||||
# 脚本徽章
|
||||
has_script = hasattr(node, 'getPythonTag') and node.getPythonTag('script')
|
||||
badges.append(("Collision", (0.2, 0.4, 0.8, 1.0)))
|
||||
|
||||
has_script = hasattr(node, "getPythonTag") and node.getPythonTag("script")
|
||||
if has_script:
|
||||
badges.append(("脚本", (0.8, 0.4, 0.8, 1.0))) # 紫色
|
||||
|
||||
# 动画徽章(优化检测逻辑,避免重复创建Actor)
|
||||
badges.append(("Script", (0.8, 0.4, 0.8, 1.0)))
|
||||
|
||||
has_animation = False
|
||||
if node_type == "模型": # 只对模型类型进行动画检测
|
||||
# 优先使用场景标签(导入/加载时会写入)
|
||||
if node_type == "模型":
|
||||
if node.hasTag("has_animations"):
|
||||
has_animation = node.getTag("has_animations").lower() == "true"
|
||||
|
||||
# 再做轻量结构检测(不依赖 Actor)
|
||||
if not has_animation:
|
||||
try:
|
||||
has_character = node.findAllMatches("**/+Character").getNumPaths() > 0
|
||||
@ -240,34 +219,25 @@ class EditorPanelsRightMixin(
|
||||
node.setTag("can_create_actor_from_memory", "true")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 只读取已有缓存,避免属性面板在普通模型上触发高噪音 Actor 探测
|
||||
cached_result = node.getPythonTag('animation')
|
||||
cached_result = node.getPythonTag("animation")
|
||||
if cached_result is True:
|
||||
has_animation = True
|
||||
elif cached_result is False:
|
||||
has_animation = False
|
||||
else:
|
||||
# 对于非模型类型,检查已有的动画标签
|
||||
has_animation = hasattr(node, 'getPythonTag') and node.getPythonTag('animation')
|
||||
|
||||
has_animation = hasattr(node, "getPythonTag") and node.getPythonTag("animation")
|
||||
|
||||
if has_animation:
|
||||
badges.append(("动画", (0.4, 0.8, 0.4, 1.0))) # 绿色
|
||||
|
||||
# 材质徽章
|
||||
has_material = hasattr(node, 'getMaterial') and node.getMaterial()
|
||||
badges.append(("Animation", (0.4, 0.8, 0.4, 1.0)))
|
||||
|
||||
has_material = hasattr(node, "getMaterial") and node.getMaterial()
|
||||
if has_material:
|
||||
badges.append(("材质", (0.8, 0.6, 0.2, 1.0))) # 金色
|
||||
|
||||
# 绘制功能性徽章
|
||||
for badge_text, badge_color in badges:
|
||||
imgui.same_line()
|
||||
badges.append(("Material", (0.8, 0.6, 0.2, 1.0)))
|
||||
|
||||
for index, (badge_text, badge_color) in enumerate(badges):
|
||||
if index > 0:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(badge_color, f"[{badge_text}]")
|
||||
|
||||
# 如果没有特殊徽章,显示默认状态
|
||||
if not badges:
|
||||
imgui.same_line()
|
||||
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "[标准对象]")
|
||||
|
||||
def _draw_gui_properties(self, node):
|
||||
"""绘制GUI元素属性"""
|
||||
|
||||
@ -4,83 +4,105 @@ class EditorPanelsRightMaterialMixin:
|
||||
"""Auto-split mixin from editor_panels_right.py."""
|
||||
|
||||
def _draw_appearance_properties(self, node):
|
||||
"""绘制外观属性"""
|
||||
# 颜色属性
|
||||
if hasattr(node, 'getColor'):
|
||||
imgui.text("颜色")
|
||||
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.slider_float("R##color_r", color[0], 0.0, 1.0)
|
||||
if changed:
|
||||
new_color = (new_r, color[1], color[2], color[3] if len(color) > 3 else 1.0)
|
||||
node.setColor(new_color)
|
||||
color = new_color
|
||||
|
||||
changed, new_g = imgui.slider_float("G##color_g", color[1], 0.0, 1.0)
|
||||
if changed:
|
||||
new_color = (color[0], new_g, color[2], color[3] if len(color) > 3 else 1.0)
|
||||
node.setColor(new_color)
|
||||
color = new_color
|
||||
|
||||
changed, new_b = imgui.slider_float("B##color_b", color[2], 0.0, 1.0)
|
||||
if changed:
|
||||
new_color = (color[0], color[1], new_b, color[3] if len(color) > 3 else 1.0)
|
||||
node.setColor(new_color)
|
||||
color = new_color
|
||||
|
||||
# 只有当颜色有alpha通道时才显示alpha滑块
|
||||
if len(color) > 3:
|
||||
changed, new_a = imgui.slider_float("A##color_a", color[3], 0.0, 1.0)
|
||||
if changed:
|
||||
new_color = (color[0], color[1], color[2], new_a)
|
||||
node.setColor(new_color)
|
||||
color = new_color
|
||||
|
||||
# 颜色预览和选择器
|
||||
imgui.text("颜色预览")
|
||||
color_with_alpha = (color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0)
|
||||
if imgui.color_button("颜色预览##preview", color_with_alpha, 0, (100, 30)):
|
||||
# 点击颜色按钮打开颜色选择器
|
||||
self.show_color_picker(node, 'color', color_with_alpha)
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("选择颜色##color_picker_btn"):
|
||||
self.show_color_picker(node, 'color', (color.x, color.y, color.z, color.w))
|
||||
|
||||
# 透明度
|
||||
if hasattr(node, 'setTransparency') and hasattr(node, 'getTransparency'):
|
||||
imgui.text("透明度")
|
||||
current_transparency = node.getTransparency()
|
||||
# 将当前的透明度值转换为0.0-1.0范围用于显示
|
||||
display_transparency = 1.0 - current_transparency if current_transparency <= 1 else 0.0
|
||||
changed, new_transparency = imgui.slider_float("透明度", display_transparency, 0.0, 1.0)
|
||||
if changed:
|
||||
# 将0.0-1.0范围转换回Panda3D的透明度格式
|
||||
panda_transparency = int((1.0 - new_transparency) * 255)
|
||||
node.setTransparency(panda_transparency)
|
||||
|
||||
# 材质属性
|
||||
self._draw_material_properties(node)
|
||||
|
||||
# 渲染状态
|
||||
imgui.text("渲染状态")
|
||||
if imgui.button("应用材质"):
|
||||
self._apply_material_to_node(node)
|
||||
|
||||
"""绘制材质属性(Unity风格主材质入口)。"""
|
||||
materials = self.app._get_node_materials(node)
|
||||
if not materials:
|
||||
fallback_material = self.app._ensure_material_for_node(node)
|
||||
materials = [fallback_material] if fallback_material else []
|
||||
if not materials:
|
||||
imgui.text_colored((1.0, 0.5, 0.5, 1.0), "无法获取材质")
|
||||
return
|
||||
material = materials[0]
|
||||
|
||||
# 历史上可能通过 node.setColor 留下了额外染色,先清掉避免与材质主颜色打架
|
||||
try:
|
||||
if node.hasColor():
|
||||
node.clearColor()
|
||||
if hasattr(node, "clearColorScale"):
|
||||
node.clearColorScale()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
base_color = self.app._get_material_base_color(material)
|
||||
|
||||
def apply_primary_color(color):
|
||||
for current_material in materials:
|
||||
self.app._set_material_base_color(current_material, color)
|
||||
self.app._apply_material_to_geom_states(node, current_material)
|
||||
if self.app._get_material_surface_type(current_material) == 3:
|
||||
self.app._set_material_opacity(node, current_material, color[3])
|
||||
else:
|
||||
self.app._apply_material_surface_state(node, current_material)
|
||||
|
||||
def apply_surface_type(surface_type):
|
||||
for current_material in materials:
|
||||
self.app._set_material_surface_type(
|
||||
node,
|
||||
current_material,
|
||||
surface_type,
|
||||
refresh_pipeline=False,
|
||||
)
|
||||
if materials:
|
||||
self.app._apply_material_surface_state(node, materials[0])
|
||||
self.app._refresh_pipeline_material_mode(node, materials[0])
|
||||
|
||||
def apply_opacity(opacity):
|
||||
for current_material in materials:
|
||||
self.app._set_material_opacity(node, current_material, opacity)
|
||||
|
||||
imgui.text("主颜色")
|
||||
changed, new_color = imgui.color_edit4(
|
||||
"##material_base_color",
|
||||
base_color,
|
||||
imgui.ColorEditFlags_.display_rgb.value,
|
||||
)
|
||||
if changed:
|
||||
apply_primary_color(new_color)
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button("重置材质"):
|
||||
self._reset_material(node)
|
||||
if imgui.button("颜色选择器##material_color_picker"):
|
||||
self.show_color_picker(
|
||||
target_object=None,
|
||||
property_name=None,
|
||||
initial_color=base_color,
|
||||
callback=apply_primary_color,
|
||||
)
|
||||
|
||||
surface_options = [
|
||||
("不透明", 0),
|
||||
("自发光", 1),
|
||||
("透明", 3),
|
||||
]
|
||||
current_surface = self.app._get_material_surface_type(material)
|
||||
current_surface_index = next(
|
||||
(index for index, (_, value) in enumerate(surface_options) if value == current_surface),
|
||||
0,
|
||||
)
|
||||
|
||||
imgui.text("表面类型")
|
||||
changed, selected_index = imgui.combo(
|
||||
"##material_surface_type",
|
||||
current_surface_index,
|
||||
[label for label, _ in surface_options],
|
||||
)
|
||||
if changed:
|
||||
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)
|
||||
changed, new_opacity = imgui.slider_float("透明度", opacity, 0.0, 1.0)
|
||||
if changed:
|
||||
apply_opacity(new_opacity)
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 详细材质属性
|
||||
self._draw_material_properties(node)
|
||||
|
||||
def _draw_material_properties(self, node):
|
||||
"""绘制材质属性"""
|
||||
materials = node.find_all_materials()
|
||||
materials = self.app._get_node_materials(node)
|
||||
|
||||
if not materials:
|
||||
imgui.text_colored((0.5, 0.5, 0.5, 1.0), "无材质")
|
||||
@ -90,33 +112,9 @@ class EditorPanelsRightMaterialMixin:
|
||||
material_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}"
|
||||
|
||||
if imgui.collapsing_header(f"材质: {material_name}"):
|
||||
# 材质基础颜色
|
||||
base_color = self._get_material_base_color(material)
|
||||
if base_color:
|
||||
imgui.text("基础颜色")
|
||||
changed, new_r = imgui.slider_float(f"R##mat_r_{i}", base_color[0], 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_material_base_color(material, 'r', new_r)
|
||||
base_color = (new_r, base_color[1], base_color[2], base_color[3])
|
||||
|
||||
changed, new_g = imgui.slider_float(f"G##mat_g_{i}", base_color[1], 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_material_base_color(material, 'g', new_g)
|
||||
base_color = (base_color[0], new_g, base_color[2], base_color[3])
|
||||
|
||||
changed, new_b = imgui.slider_float(f"B##mat_b_{i}", base_color[2], 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_material_base_color(material, 'b', new_b)
|
||||
base_color = (base_color[0], base_color[1], new_b, base_color[3])
|
||||
|
||||
changed, new_a = imgui.slider_float(f"A##mat_a_{i}", base_color[3], 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_material_base_color(material, 'a', new_a)
|
||||
base_color = (base_color[0], base_color[1], base_color[2], new_a)
|
||||
|
||||
# PBR属性
|
||||
imgui.text("PBR")
|
||||
if hasattr(material, 'roughness') and material.roughness is not None:
|
||||
imgui.text("PBR属性")
|
||||
try:
|
||||
roughness_value = float(material.roughness)
|
||||
changed, new_roughness = imgui.slider_float(f"粗糙度##rough_{i}", roughness_value, 0.0, 1.0)
|
||||
@ -152,6 +150,7 @@ class EditorPanelsRightMaterialMixin:
|
||||
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)
|
||||
imgui.end_combo()
|
||||
|
||||
# 纹理信息
|
||||
@ -178,47 +177,42 @@ class EditorPanelsRightMaterialMixin:
|
||||
if imgui.button(f"清除所有贴图##clear_{i}"):
|
||||
self._clear_all_textures(node)
|
||||
|
||||
# 着色模型选择
|
||||
self._draw_shading_model_panel(material, i)
|
||||
|
||||
# 显示当前纹理信息
|
||||
self._display_current_textures(node, material)
|
||||
|
||||
def _draw_shading_model_panel(self, material, material_index):
|
||||
imgui.separator()
|
||||
if imgui.button("应用材质"):
|
||||
self._apply_material_to_node(node)
|
||||
imgui.same_line()
|
||||
if imgui.button("重置材质"):
|
||||
self._reset_material(node)
|
||||
|
||||
def _draw_shading_model_panel(self, node, material, material_index):
|
||||
"""绘制着色模型选择面板"""
|
||||
try:
|
||||
imgui.text("着色模型")
|
||||
|
||||
# RenderPipeline支持的着色模型
|
||||
shading_models = ["默认", "自发光", "透明"]
|
||||
current_model = 0 # 默认选择
|
||||
|
||||
# 安全地获取当前着色模型
|
||||
try:
|
||||
if hasattr(material, 'emission') and material.emission is not None:
|
||||
current_model = int(material.emission.x)
|
||||
except:
|
||||
current_model = 0
|
||||
|
||||
# 着色模型选择
|
||||
if imgui.begin_combo(f"着色模型##shading_{material_index}", shading_models[current_model]):
|
||||
for j, model_name in enumerate(shading_models):
|
||||
if imgui.selectable(model_name, j == current_model):
|
||||
self._update_shading_model(material, j)
|
||||
|
||||
shading_models = [
|
||||
("默认", 0),
|
||||
("自发光", 1),
|
||||
("透明", 3),
|
||||
]
|
||||
current_model = self.app._get_material_surface_type(material)
|
||||
current_index = next((idx for idx, (_, value) in enumerate(shading_models) if value == current_model), 0)
|
||||
|
||||
if imgui.begin_combo(f"着色模型##shading_{material_index}", shading_models[current_index][0]):
|
||||
for index, (model_name, model_value) in enumerate(shading_models):
|
||||
if imgui.selectable(model_name, index == current_index):
|
||||
self.app._set_material_surface_type(node, material, model_value)
|
||||
imgui.end_combo()
|
||||
|
||||
# 如果是透明着色模型,添加透明度控制
|
||||
if current_model == 3: # 透明着色模型
|
||||
|
||||
if self.app._get_material_surface_type(material) == 3:
|
||||
imgui.text("透明度设置")
|
||||
try:
|
||||
if hasattr(material, 'shading_model_param0'):
|
||||
current_opacity = material.shading_model_param0
|
||||
else:
|
||||
current_opacity = 1.0
|
||||
|
||||
current_opacity = self.app._get_material_opacity(material)
|
||||
changed, new_opacity = imgui.slider_float(f"不透明度##opacity_{material_index}", current_opacity, 0.0, 1.0)
|
||||
if changed:
|
||||
self._update_transparency(material, new_opacity)
|
||||
self.app._set_material_opacity(node, material, new_opacity)
|
||||
except:
|
||||
imgui.text_colored((0.7, 0.7, 0.7, 1.0), "透明度控制不可用")
|
||||
|
||||
|
||||
@ -1,92 +1,147 @@
|
||||
from imgui_bundle import imgui, imgui_ctx
|
||||
from imgui_bundle import imgui
|
||||
|
||||
|
||||
class EditorPanelsRightTransformMixin:
|
||||
"""Auto-split mixin from editor_panels_right.py."""
|
||||
"""Compact Unity-style transform editor."""
|
||||
|
||||
def _draw_transform_properties(self, node):
|
||||
"""绘制变换属性"""
|
||||
# 位置组
|
||||
if imgui.collapsing_header("位置 Position"):
|
||||
# 相对位置
|
||||
imgui.text("相对位置")
|
||||
pos = node.getPos()
|
||||
|
||||
# X坐标
|
||||
changed, new_x = imgui.input_float("X##pos_x", pos.x, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setX(new_x)
|
||||
|
||||
# Y坐标
|
||||
changed, new_y = imgui.input_float("Y##pos_y", pos.y, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setY(new_y)
|
||||
|
||||
# Z坐标
|
||||
changed, new_z = imgui.input_float("Z##pos_z", pos.z, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setZ(new_z)
|
||||
|
||||
# 世界位置
|
||||
imgui.text("世界位置")
|
||||
world_pos = node.getPos(self.render)
|
||||
|
||||
imgui.text(f"世界 X: {world_pos.x:.3f}")
|
||||
imgui.text(f"世界 Y: {world_pos.y:.3f}")
|
||||
imgui.text(f"世界 Z: {world_pos.z:.3f}")
|
||||
|
||||
# 位置操作按钮
|
||||
if imgui.button("重置位置##reset_pos"):
|
||||
node.setPos(0, 0, 0)
|
||||
imgui.same_line()
|
||||
if imgui.button("复制位置##copy_pos"):
|
||||
self._clipboard_pos = (pos.x, pos.y, pos.z)
|
||||
imgui.same_line()
|
||||
if imgui.button("粘贴位置##paste_pos") and hasattr(self, '_clipboard_pos'):
|
||||
node.setPos(self._clipboard_pos[0], self._clipboard_pos[1], self._clipboard_pos[2])
|
||||
|
||||
# 旋转组
|
||||
if imgui.collapsing_header("旋转 Rotation"):
|
||||
hpr = node.getHpr()
|
||||
|
||||
# HPR旋转
|
||||
imgui.text("HPR 旋转 (度)")
|
||||
changed, new_h = imgui.input_float("H##rot_h", hpr.x, 1.0, 10.0, "%.1f")
|
||||
if changed: node.setH(new_h)
|
||||
|
||||
changed, new_p = imgui.input_float("P##rot_p", hpr.y, 1.0, 10.0, "%.1f")
|
||||
if changed: node.setP(new_p)
|
||||
|
||||
changed, new_r = imgui.input_float("R##rot_r", hpr.z, 1.0, 10.0, "%.1f")
|
||||
if changed: node.setR(new_r)
|
||||
|
||||
# 旋转操作按钮
|
||||
if imgui.button("重置旋转##reset_rot"):
|
||||
node.setHpr(0, 0, 0)
|
||||
imgui.same_line()
|
||||
if imgui.button("随机旋转##random_rot"):
|
||||
import random
|
||||
node.setHpr(random.randint(0, 360), random.randint(0, 360), random.randint(0, 360))
|
||||
|
||||
# 缩放组
|
||||
if imgui.collapsing_header("缩放 Scale"):
|
||||
imgui.push_style_var(imgui.StyleVar_.frame_padding, (6.0, 4.0))
|
||||
imgui.push_style_var(imgui.StyleVar_.item_spacing, (6.0, 4.0))
|
||||
try:
|
||||
position = node.getPos()
|
||||
rotation = node.getHpr()
|
||||
scale = node.getScale()
|
||||
|
||||
# XYZ缩放
|
||||
imgui.text("XYZ 缩放")
|
||||
changed, new_sx = imgui.input_float("X##scale_x", scale.x, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setSx(new_sx)
|
||||
|
||||
changed, new_sy = imgui.input_float("Y##scale_y", scale.y, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setSy(new_sy)
|
||||
|
||||
changed, new_sz = imgui.input_float("Z##scale_z", scale.z, 0.1, 1.0, "%.3f")
|
||||
if changed: node.setSz(new_sz)
|
||||
|
||||
# 统一缩放
|
||||
if imgui.button("统一缩放##uniform_scale"):
|
||||
uniform_scale = (scale.x + scale.y + scale.z) / 3.0
|
||||
node.setScale(uniform_scale, uniform_scale, uniform_scale)
|
||||
imgui.same_line()
|
||||
if imgui.button("重置缩放##reset_scale"):
|
||||
node.setScale(1, 1, 1)
|
||||
imgui.same_line()
|
||||
if imgui.button("翻倍##double_scale"):
|
||||
node.setScale(scale.x * 2, scale.y * 2, scale.z * 2)
|
||||
|
||||
self._draw_transform_row(
|
||||
"位置",
|
||||
"position",
|
||||
(position.x, position.y, position.z),
|
||||
lambda values: node.setPos(*values),
|
||||
speed=0.05,
|
||||
value_format="%.3f",
|
||||
reset_values=(0.0, 0.0, 0.0),
|
||||
)
|
||||
self._draw_transform_row(
|
||||
"旋转",
|
||||
"rotation",
|
||||
(rotation.x, rotation.y, rotation.z),
|
||||
lambda values: node.setHpr(*values),
|
||||
speed=0.25,
|
||||
value_format="%.2f",
|
||||
reset_values=(0.0, 0.0, 0.0),
|
||||
)
|
||||
self._draw_transform_row(
|
||||
"缩放",
|
||||
"scale",
|
||||
(scale.x, scale.y, scale.z),
|
||||
lambda values: node.setScale(*values),
|
||||
speed=0.02,
|
||||
value_format="%.3f",
|
||||
reset_values=(1.0, 1.0, 1.0),
|
||||
linked=getattr(self, "_transform_scale_locked", True),
|
||||
show_lock=True,
|
||||
)
|
||||
finally:
|
||||
imgui.pop_style_var(2)
|
||||
|
||||
def _draw_transform_row(
|
||||
self,
|
||||
label,
|
||||
row_id,
|
||||
values,
|
||||
apply_callback,
|
||||
speed,
|
||||
value_format,
|
||||
reset_values,
|
||||
linked=False,
|
||||
show_lock=False,
|
||||
):
|
||||
axis_names = ("X", "Y", "Z")
|
||||
axis_colors = (
|
||||
(0.85, 0.32, 0.32, 1.0),
|
||||
(0.36, 0.78, 0.42, 1.0),
|
||||
(0.30, 0.55, 0.92, 1.0),
|
||||
)
|
||||
current_values = [float(value) for value in values]
|
||||
original_values = list(current_values)
|
||||
changed_any = False
|
||||
|
||||
table_flags = (
|
||||
imgui.TableFlags_.sizing_stretch_same.value
|
||||
| imgui.TableFlags_.no_pad_outer_x.value
|
||||
| imgui.TableFlags_.no_borders_in_body.value
|
||||
)
|
||||
if imgui.begin_table(f"{row_id}_table", 8, table_flags):
|
||||
imgui.table_setup_column("label", imgui.TableColumnFlags_.width_fixed.value, 52.0)
|
||||
imgui.table_setup_column("lock", imgui.TableColumnFlags_.width_fixed.value, 22.0)
|
||||
for axis_name in axis_names:
|
||||
imgui.table_setup_column(f"{axis_name}_label", imgui.TableColumnFlags_.width_fixed.value, 16.0)
|
||||
imgui.table_setup_column(f"{axis_name}_value", imgui.TableColumnFlags_.width_stretch.value, 1.0)
|
||||
|
||||
imgui.table_next_row()
|
||||
imgui.table_next_column()
|
||||
imgui.text_disabled(label)
|
||||
|
||||
imgui.table_next_column()
|
||||
if show_lock:
|
||||
changed, self._transform_scale_locked = imgui.checkbox(
|
||||
f"##{row_id}_lock",
|
||||
getattr(self, "_transform_scale_locked", True),
|
||||
)
|
||||
if imgui.is_item_hovered():
|
||||
imgui.set_tooltip("锁定 XYZ 缩放比例")
|
||||
else:
|
||||
imgui.text("")
|
||||
|
||||
for axis_index, axis_name in enumerate(axis_names):
|
||||
imgui.table_next_column()
|
||||
imgui.text_colored(axis_colors[axis_index], axis_name)
|
||||
|
||||
imgui.table_next_column()
|
||||
imgui.set_next_item_width(-1)
|
||||
axis_changed, new_value = imgui.drag_float(
|
||||
f"##{row_id}_{axis_name}",
|
||||
current_values[axis_index],
|
||||
speed,
|
||||
format=value_format,
|
||||
)
|
||||
if axis_changed:
|
||||
current_values = self._apply_vector_value_change(
|
||||
original_values,
|
||||
current_values,
|
||||
axis_index,
|
||||
float(new_value),
|
||||
linked,
|
||||
reset_values,
|
||||
)
|
||||
changed_any = True
|
||||
|
||||
imgui.end_table()
|
||||
|
||||
if changed_any:
|
||||
apply_callback(tuple(current_values))
|
||||
|
||||
def _apply_vector_value_change(
|
||||
self,
|
||||
original_values,
|
||||
current_values,
|
||||
axis_index,
|
||||
new_value,
|
||||
linked,
|
||||
reset_values,
|
||||
):
|
||||
next_values = list(current_values)
|
||||
if not linked:
|
||||
next_values[axis_index] = new_value
|
||||
return next_values
|
||||
|
||||
old_value = original_values[axis_index]
|
||||
if abs(old_value) > 1e-6:
|
||||
ratio = new_value / old_value
|
||||
return [value * ratio for value in original_values]
|
||||
|
||||
if abs(reset_values[axis_index]) > 1e-6:
|
||||
ratio = new_value / float(reset_values[axis_index])
|
||||
return [float(default_value) * ratio for default_value in reset_values]
|
||||
|
||||
return [new_value, new_value, new_value]
|
||||
|
||||
@ -141,21 +141,35 @@ class EditorPanelsTopMixin:
|
||||
# 视图菜单
|
||||
with imgui_ctx.begin_menu("视图") as view_menu:
|
||||
if view_menu:
|
||||
_, self.app.showToolbar = imgui.menu_item("工具栏", "", self.app.showToolbar, True)
|
||||
_, self.app.showSceneTree = imgui.menu_item("场景树", "", self.app.showSceneTree, True)
|
||||
_, self.app.showResourceManager = imgui.menu_item("资源管理器", "", self.app.showResourceManager, True)
|
||||
_, self.app.showPropertyPanel = imgui.menu_item("属性面板", "", self.app.showPropertyPanel, True)
|
||||
_, self.app.showConsole = imgui.menu_item("控制台", "", self.app.showConsole, True)
|
||||
_, self.app.showScriptPanel = imgui.menu_item("脚本管理", "", self.app.showScriptPanel, True)
|
||||
_, self.app.showLUIEditor = imgui.menu_item("LUI编辑器", "", self.app.showLUIEditor, True)
|
||||
if hasattr(self.app, "lui_manager"):
|
||||
self.app.lui_manager.show_editor = self.app.showLUIEditor
|
||||
prev_show_web_panel = self.app.showWebPanel
|
||||
_, self.app.showWebPanel = imgui.menu_item("Web面板", "", self.app.showWebPanel, True)
|
||||
if prev_show_web_panel and not self.app.showWebPanel:
|
||||
self._stop_imgui_webview()
|
||||
elif (not prev_show_web_panel) and self.app.showWebPanel:
|
||||
self._on_create_web_panel()
|
||||
changed, visible = imgui.menu_item("工具栏", "", self.app.is_panel_visible("toolbar"), True)
|
||||
if changed:
|
||||
self.app.set_panel_visible("toolbar", visible)
|
||||
changed, visible = imgui.menu_item("场景树", "", self.app.is_panel_visible("scene_tree"), True)
|
||||
if changed:
|
||||
self.app.set_panel_visible("scene_tree", visible)
|
||||
changed, visible = imgui.menu_item("资源管理器", "", self.app.is_panel_visible("resources"), True)
|
||||
if changed:
|
||||
self.app.set_panel_visible("resources", visible)
|
||||
changed, visible = imgui.menu_item("属性面板", "", self.app.is_panel_visible("property"), True)
|
||||
if changed:
|
||||
self.app.set_panel_visible("property", visible)
|
||||
changed, visible = imgui.menu_item("控制台", "", self.app.is_panel_visible("console"), True)
|
||||
if changed:
|
||||
self.app.set_panel_visible("console", visible)
|
||||
changed, visible = imgui.menu_item("脚本管理", "", self.app.is_panel_visible("script"), True)
|
||||
if changed:
|
||||
self.app.set_panel_visible("script", visible)
|
||||
changed, visible = imgui.menu_item("LUI编辑器", "", self.app.is_panel_visible("lui_editor"), True)
|
||||
if changed:
|
||||
self.app.set_panel_visible("lui_editor", visible)
|
||||
changed, visible = imgui.menu_item("Web面板", "", self.app.is_panel_visible("web"), True)
|
||||
if changed:
|
||||
self.app.set_panel_visible("web", visible)
|
||||
imgui.separator()
|
||||
if imgui.menu_item("恢复默认面板", "", False, True)[1]:
|
||||
self.app.reset_panel_visibility_to_defaults()
|
||||
if imgui.menu_item("重置布局", "", False, True)[1]:
|
||||
self.app._reset_imgui_layout()
|
||||
|
||||
# 工具菜单
|
||||
with imgui_ctx.begin_menu("工具") as tools_menu:
|
||||
|
||||
@ -247,6 +247,36 @@ class PanelDelegates:
|
||||
def _get_material_base_color(self, *args, **kwargs):
|
||||
return self.property_helpers._get_material_base_color(*args, **kwargs)
|
||||
|
||||
def _get_node_materials(self, *args, **kwargs):
|
||||
return self.property_helpers._get_node_materials(*args, **kwargs)
|
||||
|
||||
def _ensure_material_for_node(self, *args, **kwargs):
|
||||
return self.property_helpers._ensure_material_for_node(*args, **kwargs)
|
||||
|
||||
def _get_material_surface_type(self, *args, **kwargs):
|
||||
return self.property_helpers._get_material_surface_type(*args, **kwargs)
|
||||
|
||||
def _set_material_surface_type(self, *args, **kwargs):
|
||||
return self.property_helpers._set_material_surface_type(*args, **kwargs)
|
||||
|
||||
def _refresh_pipeline_material_mode(self, *args, **kwargs):
|
||||
return self.property_helpers._refresh_pipeline_material_mode(*args, **kwargs)
|
||||
|
||||
def _get_material_opacity(self, *args, **kwargs):
|
||||
return self.property_helpers._get_material_opacity(*args, **kwargs)
|
||||
|
||||
def _set_material_opacity(self, *args, **kwargs):
|
||||
return self.property_helpers._set_material_opacity(*args, **kwargs)
|
||||
|
||||
def _apply_material_surface_state(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_material_surface_state(*args, **kwargs)
|
||||
|
||||
def _set_material_base_color(self, *args, **kwargs):
|
||||
return self.property_helpers._set_material_base_color(*args, **kwargs)
|
||||
|
||||
def _apply_material_to_geom_states(self, *args, **kwargs):
|
||||
return self.property_helpers._apply_material_to_geom_states(*args, **kwargs)
|
||||
|
||||
def _update_material_base_color(self, *args, **kwargs):
|
||||
return self.property_helpers._update_material_base_color(*args, **kwargs)
|
||||
|
||||
|
||||
@ -626,8 +626,543 @@ class PropertyHelpers:
|
||||
return (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||||
except:
|
||||
return (1.0, 1.0, 1.0, 1.0) # 默认白色
|
||||
|
||||
|
||||
def _apply_material_to_geom_states(self, node, material):
|
||||
"""Bake the editable material into every GeomState so RP can see runtime edits."""
|
||||
try:
|
||||
from panda3d.core import MaterialAttrib
|
||||
|
||||
if not node or node.isEmpty() or material is None:
|
||||
return
|
||||
|
||||
# For imported multi-material models we edit the existing material
|
||||
# instances in place. Rebroadcasting one material to every GeomNode
|
||||
# would collapse the whole model to a single material.
|
||||
try:
|
||||
if not node.hasMaterial():
|
||||
self._invalidate_material_render_cache()
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for geom_path in node.find_all_matches("**/+GeomNode"):
|
||||
try:
|
||||
geom_path.setState(geom_path.getState().setAttrib(MaterialAttrib.make(material)))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
geom_node = geom_path.node()
|
||||
for i in range(geom_node.getNumGeoms()):
|
||||
try:
|
||||
geom_state = geom_node.getGeomState(i)
|
||||
geom_node.setGeomState(i, geom_state.setAttrib(MaterialAttrib.make(material)))
|
||||
except Exception:
|
||||
pass
|
||||
self._invalidate_material_render_cache()
|
||||
except Exception as e:
|
||||
print(f"同步Geom材质状态失败: {e}")
|
||||
|
||||
def _get_node_materials(self, node):
|
||||
"""Return the editable materials currently used by a node."""
|
||||
if not node or node.isEmpty():
|
||||
return []
|
||||
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
return [node.getMaterial()]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
materials = list(node.find_all_materials())
|
||||
except Exception:
|
||||
materials = []
|
||||
|
||||
unique_materials = []
|
||||
seen_keys = set()
|
||||
for material in materials:
|
||||
key = getattr(material, "this", None) or id(material)
|
||||
if key in seen_keys:
|
||||
continue
|
||||
seen_keys.add(key)
|
||||
unique_materials.append(material)
|
||||
return unique_materials
|
||||
|
||||
def _invalidate_material_render_cache(self):
|
||||
"""Force Panda/RenderPipeline to pick up runtime material edits immediately."""
|
||||
try:
|
||||
from panda3d.core import RenderState
|
||||
RenderState.clear_cache()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _ensure_material_for_node(self, node):
|
||||
"""Ensure a node has at least one editable material and return the primary material."""
|
||||
try:
|
||||
if node and not node.isEmpty() and node.hasMaterial():
|
||||
material = node.getMaterial()
|
||||
self._apply_material_to_geom_states(node, material)
|
||||
return material
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
materials = self._get_node_materials(node)
|
||||
if materials:
|
||||
return materials[0]
|
||||
|
||||
try:
|
||||
from panda3d.core import Material, Vec4
|
||||
|
||||
material = Material(f"default-material-{node.getName() or 'node'}")
|
||||
material.set_base_color(Vec4(0.8, 0.8, 0.8, 1.0))
|
||||
material.set_roughness(0.5)
|
||||
material.set_metallic(0.0)
|
||||
material.set_refractive_index(1.5)
|
||||
material.set_emission(Vec4(0.0, 0.0, 1.0, 0.0))
|
||||
node.setMaterial(material, 1)
|
||||
self._apply_material_to_geom_states(node, material)
|
||||
return material
|
||||
except Exception as e:
|
||||
print(f"创建默认材质失败: {e}")
|
||||
return None
|
||||
|
||||
def _get_material_surface_type(self, material):
|
||||
"""Return the RenderPipeline shading model value used by this material."""
|
||||
try:
|
||||
emission = material.emission if hasattr(material, "emission") else None
|
||||
if emission is None:
|
||||
return 0
|
||||
shading_model = int(round(float(emission.x)))
|
||||
if shading_model in (0, 1, 3):
|
||||
return shading_model
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
def _set_material_surface_type(self, node, material, surface_type, refresh_pipeline=True):
|
||||
"""Update material shading model and sync node transparency state."""
|
||||
try:
|
||||
from panda3d.core import Vec4
|
||||
|
||||
surface_type = int(surface_type)
|
||||
previous_surface_type = self._get_material_surface_type(material)
|
||||
emission = material.emission if hasattr(material, "emission") and material.emission is not None else Vec4(0, 0, 1, 0)
|
||||
if surface_type == 3:
|
||||
if previous_surface_type == 3:
|
||||
opacity = self._get_material_opacity(material)
|
||||
else:
|
||||
base_alpha = float(self._get_material_base_color(material)[3])
|
||||
opacity = base_alpha if 0.0 < base_alpha <= 1.0 else 1.0
|
||||
else:
|
||||
opacity = 1.0
|
||||
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity, float(emission.w)))
|
||||
self._apply_material_to_geom_states(node, material)
|
||||
self._apply_material_surface_state(node, material)
|
||||
if refresh_pipeline:
|
||||
self._refresh_pipeline_material_mode(node, material)
|
||||
except Exception as e:
|
||||
print(f"设置材质表面类型失败: {e}")
|
||||
|
||||
def _get_material_opacity(self, material):
|
||||
"""Return opacity for transparent materials."""
|
||||
try:
|
||||
if self._get_material_surface_type(material) != 3:
|
||||
return 1.0
|
||||
emission = material.emission if hasattr(material, "emission") else None
|
||||
if emission is not None:
|
||||
return max(0.0, min(1.0, float(emission.z)))
|
||||
except Exception:
|
||||
pass
|
||||
return 1.0
|
||||
|
||||
def _material_uses_transparent_pass(self, material):
|
||||
"""Return whether the material should be rendered through RP forward transparency."""
|
||||
try:
|
||||
return self._get_material_surface_type(material) == 3
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _refresh_pipeline_material_mode(self, node, material):
|
||||
"""Let RenderPipeline re-evaluate transparent material routing for this subtree."""
|
||||
try:
|
||||
render_pipeline = getattr(self, "render_pipeline", None)
|
||||
if not render_pipeline or not node or node.isEmpty():
|
||||
return
|
||||
if self._material_uses_transparent_pass(material) and hasattr(render_pipeline, "prepare_scene"):
|
||||
self._bake_effective_geom_materials(node)
|
||||
self._isolate_transparent_geoms(node)
|
||||
render_pipeline.prepare_scene(node)
|
||||
except Exception as e:
|
||||
print(f"刷新RenderPipeline材质模式失败: {e}")
|
||||
|
||||
def _bake_effective_geom_materials(self, node):
|
||||
"""Bake inherited/material override state back into each GeomState for RP scene analysis."""
|
||||
try:
|
||||
from panda3d.core import MaterialAttrib
|
||||
|
||||
if not node or node.isEmpty():
|
||||
return False
|
||||
|
||||
changed = False
|
||||
for geom_np in node.find_all_matches("**/+GeomNode"):
|
||||
geom_node = geom_np.node()
|
||||
net_state = None
|
||||
try:
|
||||
net_state = geom_np.getNetState()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for geom_index in range(geom_node.getNumGeoms()):
|
||||
try:
|
||||
geom_state = geom_node.getGeomState(geom_index)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
material = None
|
||||
try:
|
||||
if geom_state.hasAttrib(MaterialAttrib):
|
||||
material_attrib = geom_state.getAttrib(MaterialAttrib)
|
||||
material = material_attrib.getMaterial() if material_attrib else None
|
||||
except Exception:
|
||||
material = None
|
||||
|
||||
if material is None and net_state is not None:
|
||||
try:
|
||||
if net_state.hasAttrib(MaterialAttrib):
|
||||
material_attrib = net_state.getAttrib(MaterialAttrib)
|
||||
material = material_attrib.getMaterial() if material_attrib else None
|
||||
except Exception:
|
||||
material = None
|
||||
|
||||
if material is None:
|
||||
try:
|
||||
if geom_np.hasMaterial():
|
||||
material = geom_np.getMaterial()
|
||||
except Exception:
|
||||
material = None
|
||||
|
||||
if material is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
geom_node.setGeomState(geom_index, geom_state.setAttrib(MaterialAttrib.make(material)))
|
||||
changed = True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if changed:
|
||||
self._invalidate_material_render_cache()
|
||||
return changed
|
||||
except Exception as e:
|
||||
print(f"烘焙Geom材质状态失败: {e}")
|
||||
return False
|
||||
|
||||
def _get_renderable_node_material(self, node):
|
||||
"""Resolve the material that should represent a renderable GeomNode."""
|
||||
try:
|
||||
if not node or node.isEmpty():
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
if node.hasMaterial():
|
||||
material = node.getMaterial()
|
||||
if material is not None:
|
||||
return material
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
geom_node = node.node()
|
||||
for geom_index in range(geom_node.getNumGeoms()):
|
||||
geom_state = geom_node.getGeomState(geom_index)
|
||||
if not geom_state.hasAttrib(MaterialAttrib):
|
||||
continue
|
||||
material_attrib = geom_state.getAttrib(MaterialAttrib)
|
||||
material = material_attrib.getMaterial() if material_attrib else None
|
||||
if material is not None:
|
||||
return material
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
materials = self._get_node_materials(node)
|
||||
if materials:
|
||||
return materials[0]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _isolate_transparent_geoms(self, node):
|
||||
"""Split multi-geom GeomNodes so RP transparent materials can be routed correctly."""
|
||||
try:
|
||||
from panda3d.core import GeomNode, MaterialAttrib
|
||||
|
||||
if not node or node.isEmpty():
|
||||
return False
|
||||
|
||||
geom_paths = [match for match in node.find_all_matches("**/+GeomNode")]
|
||||
changed = False
|
||||
|
||||
for geom_np in geom_paths:
|
||||
try:
|
||||
geom_node = geom_np.node()
|
||||
geom_count = geom_node.getNumGeoms()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if geom_count <= 1:
|
||||
continue
|
||||
|
||||
has_transparent_geom = False
|
||||
for geom_index in range(geom_count):
|
||||
try:
|
||||
geom_state = geom_node.getGeomState(geom_index)
|
||||
if not geom_state.hasAttrib(MaterialAttrib):
|
||||
continue
|
||||
material_attrib = geom_state.getAttrib(MaterialAttrib)
|
||||
geom_material = material_attrib.getMaterial() if material_attrib else None
|
||||
if geom_material and self._get_material_surface_type(geom_material) == 3:
|
||||
has_transparent_geom = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not has_transparent_geom:
|
||||
continue
|
||||
|
||||
parent_np = geom_np.getParent()
|
||||
if not parent_np or parent_np.isEmpty():
|
||||
continue
|
||||
|
||||
try:
|
||||
local_transform = geom_np.getTransform(parent_np)
|
||||
except Exception:
|
||||
local_transform = None
|
||||
try:
|
||||
local_mat = geom_np.getMat(parent_np)
|
||||
except Exception:
|
||||
local_mat = None
|
||||
try:
|
||||
path_state = geom_np.getState()
|
||||
except Exception:
|
||||
path_state = None
|
||||
|
||||
tag_keys = []
|
||||
python_tag_keys = []
|
||||
try:
|
||||
tag_keys = list(geom_np.getTagKeys())
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
python_tag_keys = list(geom_np.getPythonTagKeys())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for geom_index in range(geom_count):
|
||||
try:
|
||||
split_geom = geom_node.getGeom(geom_index).makeCopy()
|
||||
split_state = geom_node.getGeomState(geom_index)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
effective_material = None
|
||||
|
||||
try:
|
||||
if (not split_state.hasAttrib(MaterialAttrib)) or split_state.getAttrib(MaterialAttrib).getMaterial() is None:
|
||||
effective_state = geom_np.getNetState()
|
||||
if effective_state.hasAttrib(MaterialAttrib):
|
||||
effective_material = effective_state.getAttrib(MaterialAttrib).getMaterial()
|
||||
if effective_material is not None:
|
||||
split_state = split_state.setAttrib(MaterialAttrib.make(effective_material))
|
||||
else:
|
||||
effective_material = split_state.getAttrib(MaterialAttrib).getMaterial()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
split_name = geom_node.getName() if geom_count == 1 else f"{geom_node.getName()}__geom_{geom_index}"
|
||||
split_node = GeomNode(split_name)
|
||||
split_node.addGeom(split_geom, split_state)
|
||||
split_np = parent_np.attachNewNode(split_node)
|
||||
|
||||
if local_transform is not None:
|
||||
try:
|
||||
split_np.setTransform(parent_np, local_transform)
|
||||
except Exception:
|
||||
pass
|
||||
elif local_mat is not None:
|
||||
try:
|
||||
split_np.setMat(parent_np, local_mat)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if path_state is not None:
|
||||
try:
|
||||
split_np.setState(path_state)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if effective_material is not None:
|
||||
try:
|
||||
split_np.setMaterial(effective_material, 1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if geom_np.isHidden():
|
||||
split_np.hide()
|
||||
|
||||
for tag_key in tag_keys:
|
||||
try:
|
||||
split_np.setTag(tag_key, geom_np.getTag(tag_key))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for tag_key in python_tag_keys:
|
||||
try:
|
||||
split_np.setPythonTag(tag_key, geom_np.getPythonTag(tag_key))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
geom_np.removeNode()
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
self._invalidate_material_render_cache()
|
||||
return changed
|
||||
except Exception as e:
|
||||
print(f"拆分透明几何节点失败: {e}")
|
||||
return False
|
||||
|
||||
def _iter_material_state_nodes(self, node):
|
||||
"""Yield the concrete renderable nodes that should receive material state updates."""
|
||||
try:
|
||||
if not node or node.isEmpty():
|
||||
return []
|
||||
renderable_nodes = [match for match in node.find_all_matches("**/+GeomNode")]
|
||||
if renderable_nodes:
|
||||
return renderable_nodes
|
||||
except Exception:
|
||||
pass
|
||||
return [node]
|
||||
|
||||
def _sync_material_render_effect(self, node, material=None, force=False, source_node=None):
|
||||
"""Keep RenderPipeline effect options in sync with the current material surface mode."""
|
||||
try:
|
||||
if not node or node.isEmpty():
|
||||
return
|
||||
|
||||
render_pipeline = getattr(self, "render_pipeline", None)
|
||||
if not render_pipeline:
|
||||
return
|
||||
|
||||
material = material or self._ensure_material_for_node(node)
|
||||
if not material:
|
||||
return
|
||||
|
||||
source_node = source_node or node
|
||||
use_forward = self._material_uses_transparent_pass(material)
|
||||
use_metallic_effect = source_node.hasTag("material_effect_metallic_enabled")
|
||||
enable_parallax = source_node.hasTag("material_effect_parallax_enabled")
|
||||
|
||||
effect_path = "effects/pbr_with_metallic.yaml" if use_metallic_effect else "effects/default.yaml"
|
||||
options = {
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": not use_forward,
|
||||
"render_forward": use_forward,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": enable_parallax,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True,
|
||||
}
|
||||
sort = 60 if use_metallic_effect else 55
|
||||
effect_signature = "|".join((
|
||||
effect_path,
|
||||
"1" if options["render_forward"] else "0",
|
||||
"1" if options["render_gbuffer"] else "0",
|
||||
"1" if options["parallax_mapping"] else "0",
|
||||
"1" if use_metallic_effect else "0",
|
||||
))
|
||||
current_signature = node.getTag("material_render_effect_signature") if node.hasTag("material_render_effect_signature") else ""
|
||||
if not force and current_signature == effect_signature:
|
||||
return
|
||||
|
||||
apply_effect = getattr(render_pipeline, "_internal_set_effect", None) or getattr(render_pipeline, "set_effect", None)
|
||||
if not apply_effect:
|
||||
return
|
||||
apply_effect(node, effect_path, options, sort)
|
||||
node.setTag("material_render_effect_signature", effect_signature)
|
||||
except Exception as e:
|
||||
print(f"同步材质渲染 effect 失败: {e}")
|
||||
|
||||
def _set_material_opacity(self, node, material, opacity_value):
|
||||
"""Update transparent opacity in the RenderPipeline-compatible slot."""
|
||||
try:
|
||||
from panda3d.core import Vec4
|
||||
|
||||
opacity_value = max(0.0, min(1.0, float(opacity_value)))
|
||||
emission = material.emission if hasattr(material, "emission") and material.emission is not None else Vec4(3, 0, 1, 0)
|
||||
surface_type = self._get_material_surface_type(material)
|
||||
if surface_type != 3:
|
||||
surface_type = 3
|
||||
material.set_emission(Vec4(float(surface_type), float(emission.y), opacity_value, float(emission.w)))
|
||||
|
||||
base_color = list(self._get_material_base_color(material))
|
||||
base_color[3] = opacity_value
|
||||
self._set_material_base_color(material, tuple(base_color))
|
||||
self._apply_material_to_geom_states(node, material)
|
||||
self._apply_material_surface_state(node, material)
|
||||
except Exception as e:
|
||||
print(f"设置材质透明度失败: {e}")
|
||||
|
||||
def _apply_material_surface_state(self, node, material):
|
||||
"""Sync Panda node transparency mode with the material surface type."""
|
||||
try:
|
||||
from panda3d.core import ColorBlendAttrib, TransparencyAttrib
|
||||
|
||||
for target_node in self._iter_material_state_nodes(node):
|
||||
target_material = self._get_renderable_node_material(target_node) or material
|
||||
if target_material is not None:
|
||||
try:
|
||||
target_node.setMaterial(target_material, 1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Let RenderPipeline handle transparent materials via its
|
||||
# forward pass. Leaving Panda-side alpha blending enabled here
|
||||
# causes the object to be blended twice and makes live editing
|
||||
# hard to reason about.
|
||||
target_node.setTransparency(TransparencyAttrib.M_none)
|
||||
target_node.setAlphaScale(1.0)
|
||||
target_node.setDepthWrite(True)
|
||||
try:
|
||||
target_node.clearBin()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
target_node.clearAttrib(ColorBlendAttrib.getClassType())
|
||||
except Exception:
|
||||
pass
|
||||
self._sync_material_render_effect(target_node, target_material, source_node=node)
|
||||
except Exception as e:
|
||||
print(f"同步材质透明状态失败: {e}")
|
||||
|
||||
def _set_material_base_color(self, material, color):
|
||||
"""Set material base color using the APIs available in the current Panda build."""
|
||||
try:
|
||||
from panda3d.core import Vec4
|
||||
|
||||
color_vec = Vec4(*color)
|
||||
if hasattr(material, "set_base_color"):
|
||||
material.set_base_color(color_vec)
|
||||
elif hasattr(material, "setBaseColor"):
|
||||
material.setBaseColor(color_vec)
|
||||
elif hasattr(material, "setDiffuse"):
|
||||
material.setDiffuse(color_vec)
|
||||
except Exception as e:
|
||||
print(f"设置材质基础颜色失败: {e}")
|
||||
|
||||
|
||||
def _update_material_base_color(self, material, component, value):
|
||||
"""更新材质基础颜色"""
|
||||
try:
|
||||
@ -645,12 +1180,7 @@ class PropertyHelpers:
|
||||
|
||||
new_color_tuple = tuple(new_color)
|
||||
|
||||
if hasattr(material, 'set_base_color'):
|
||||
from panda3d.core import Vec4
|
||||
material.set_base_color(Vec4(*new_color_tuple))
|
||||
elif hasattr(material, 'setDiffuse'):
|
||||
from panda3d.core import Vec4
|
||||
material.setDiffuse(Vec4(*new_color_tuple))
|
||||
self._set_material_base_color(material, new_color_tuple)
|
||||
except Exception as e:
|
||||
print(f"更新材质基础颜色失败: {e}")
|
||||
|
||||
@ -730,10 +1260,12 @@ class PropertyHelpers:
|
||||
preset = presets[preset_name]
|
||||
|
||||
# 应用基础颜色
|
||||
if hasattr(material, 'set_base_color'):
|
||||
material.set_base_color(preset["base_color"])
|
||||
elif hasattr(material, 'setDiffuse'):
|
||||
material.setDiffuse(preset["base_color"])
|
||||
self._set_material_base_color(material, (
|
||||
preset["base_color"].x,
|
||||
preset["base_color"].y,
|
||||
preset["base_color"].z,
|
||||
preset["base_color"].w,
|
||||
))
|
||||
|
||||
# 应用PBR属性
|
||||
if hasattr(material, 'set_roughness'):
|
||||
@ -742,6 +1274,10 @@ class PropertyHelpers:
|
||||
material.set_metallic(preset["metallic"])
|
||||
if hasattr(material, 'set_refractive_index'):
|
||||
material.set_refractive_index(preset["ior"])
|
||||
if hasattr(material, "set_emission"):
|
||||
surface_type = 3.0 if preset_name == "玻璃" else 0.0
|
||||
opacity = preset["base_color"].w if preset_name == "玻璃" else 1.0
|
||||
material.set_emission(Vec4(surface_type, 0.0, opacity, 0.0))
|
||||
|
||||
print(f"已应用材质预设: {preset_name}")
|
||||
except Exception as e:
|
||||
@ -751,23 +1287,10 @@ class PropertyHelpers:
|
||||
def _apply_material_to_node(self, node):
|
||||
"""为节点应用材质"""
|
||||
try:
|
||||
from panda3d.core import Material, Vec4
|
||||
|
||||
# 检查是否已有材质
|
||||
materials = node.find_all_materials()
|
||||
|
||||
if not materials:
|
||||
# 创建新材质
|
||||
material = Material(f"default-material-{node.getName()}")
|
||||
material.setBaseColor(Vec4(0.8, 0.8, 0.8, 1.0))
|
||||
material.setDiffuse(Vec4(0.8, 0.8, 0.8, 1.0))
|
||||
material.setAmbient(Vec4(0.4, 0.4, 0.4, 1.0))
|
||||
material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0))
|
||||
material.setShininess(10.0)
|
||||
node.setMaterial(material, 1)
|
||||
print(f"已为新节点创建默认材质")
|
||||
else:
|
||||
print(f"节点已有 {len(materials)} 个材质")
|
||||
material = self._ensure_material_for_node(node)
|
||||
if material:
|
||||
self._apply_material_surface_state(node, material)
|
||||
print("材质已应用到节点")
|
||||
except Exception as e:
|
||||
print(f"应用材质失败: {e}")
|
||||
|
||||
@ -775,17 +1298,14 @@ class PropertyHelpers:
|
||||
def _reset_material(self, node):
|
||||
"""重置节点材质"""
|
||||
try:
|
||||
materials = node.find_all_materials()
|
||||
materials = list(node.find_all_materials())
|
||||
|
||||
for material in materials:
|
||||
# 重置为默认材质属性
|
||||
from panda3d.core import Vec4
|
||||
default_color = Vec4(0.8, 0.8, 0.8, 1.0)
|
||||
|
||||
if hasattr(material, 'set_base_color'):
|
||||
material.set_base_color(default_color)
|
||||
elif hasattr(material, 'setDiffuse'):
|
||||
material.setDiffuse(default_color)
|
||||
self._set_material_base_color(material, (0.8, 0.8, 0.8, 1.0))
|
||||
|
||||
if hasattr(material, 'set_roughness'):
|
||||
material.set_roughness(0.5)
|
||||
@ -793,6 +1313,9 @@ class PropertyHelpers:
|
||||
material.set_metallic(0.0)
|
||||
if hasattr(material, 'set_refractive_index'):
|
||||
material.set_refractive_index(1.5)
|
||||
if hasattr(material, 'set_emission'):
|
||||
material.set_emission(Vec4(0.0, 0.0, 1.0, 0.0))
|
||||
self._apply_material_surface_state(node, material)
|
||||
|
||||
print(f"已重置材质")
|
||||
except Exception as e:
|
||||
@ -960,28 +1483,8 @@ class PropertyHelpers:
|
||||
if not node or node.isEmpty():
|
||||
return
|
||||
|
||||
render_pipeline = getattr(self, "render_pipeline", None)
|
||||
if not render_pipeline:
|
||||
return
|
||||
|
||||
# 避免重复设置 effect
|
||||
if node.hasTag("material_effect_metallic_enabled"):
|
||||
return
|
||||
|
||||
render_pipeline.set_effect(
|
||||
node,
|
||||
"effects/pbr_with_metallic.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": False,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
60
|
||||
)
|
||||
node.setTag("material_effect_metallic_enabled", "1")
|
||||
self._sync_material_render_effect(node)
|
||||
except Exception as e:
|
||||
print(f"启用金属性贴图 effect 失败: {e}")
|
||||
|
||||
@ -992,10 +1495,6 @@ class PropertyHelpers:
|
||||
if not node or node.isEmpty():
|
||||
return
|
||||
|
||||
render_pipeline = getattr(self, "render_pipeline", None)
|
||||
if not render_pipeline:
|
||||
return
|
||||
|
||||
# 已启用金属性增强 effect 时,不覆盖它
|
||||
if node.hasTag("material_effect_metallic_enabled"):
|
||||
return
|
||||
@ -1004,21 +1503,8 @@ class PropertyHelpers:
|
||||
if parallax_enabled:
|
||||
node.setTag("material_effect_parallax_enabled", "1")
|
||||
|
||||
# 为了避免因未知默认值导致 normal mapping 关闭,这里显式设置
|
||||
render_pipeline.set_effect(
|
||||
node,
|
||||
"effects/default.yaml",
|
||||
{
|
||||
"normal_mapping": True,
|
||||
"render_gbuffer": True,
|
||||
"alpha_testing": False,
|
||||
"parallax_mapping": parallax_enabled,
|
||||
"render_shadow": True,
|
||||
"render_envmap": True
|
||||
},
|
||||
55
|
||||
)
|
||||
node.setTag("material_effect_default_texture_enabled", "1")
|
||||
self._sync_material_render_effect(node)
|
||||
except Exception as e:
|
||||
print(f"启用默认贴图 effect 失败: {e}")
|
||||
|
||||
@ -1298,13 +1784,9 @@ class PropertyHelpers:
|
||||
elif model_index == 3: # 透明着色模型
|
||||
print("设置透明着色模型...")
|
||||
if hasattr(material, 'set_emission'):
|
||||
current_emission = material.emission or Vec4(0, 0, 0, 0)
|
||||
new_emission = Vec4(3.0, 0, 0, 0) # 3表示透明着色模型
|
||||
current_emission = material.emission or Vec4(0, 0, 1, 0)
|
||||
new_emission = Vec4(3.0, current_emission.y, current_emission.z, current_emission.w)
|
||||
material.set_emission(new_emission)
|
||||
|
||||
# 设置默认透明度
|
||||
if hasattr(material, 'shading_model_param0'):
|
||||
material.shading_model_param0 = 0.8 # 默认80%不透明度
|
||||
|
||||
print("透明着色模型设置完成")
|
||||
|
||||
@ -1325,8 +1807,10 @@ class PropertyHelpers:
|
||||
def _update_transparency(self, material, opacity_value):
|
||||
"""更新透明度"""
|
||||
try:
|
||||
if hasattr(material, 'shading_model_param0'):
|
||||
material.shading_model_param0 = opacity_value
|
||||
from panda3d.core import Vec4
|
||||
if hasattr(material, 'set_emission'):
|
||||
current_emission = material.emission or Vec4(3, 0, 1, 0)
|
||||
material.set_emission(Vec4(current_emission.x, current_emission.y, opacity_value, current_emission.w))
|
||||
print(f"透明度已更新: {opacity_value}")
|
||||
except Exception as e:
|
||||
print(f"更新透明度失败: {e}")
|
||||
@ -1492,6 +1976,13 @@ class PropertyHelpers:
|
||||
|
||||
def _apply_color_selection(self):
|
||||
"""应用颜色选择"""
|
||||
if self._color_picker_callback:
|
||||
try:
|
||||
self._color_picker_callback(self._color_picker_current_color)
|
||||
except Exception as e:
|
||||
print(f"颜色回调执行失败: {e}")
|
||||
return
|
||||
|
||||
if not self._color_picker_target:
|
||||
return
|
||||
|
||||
@ -1501,12 +1992,8 @@ class PropertyHelpers:
|
||||
# 应用颜色到目标对象
|
||||
if hasattr(target_object, 'setColor'):
|
||||
target_object.setColor(self._color_picker_current_color)
|
||||
elif hasattr(target_object, property_name):
|
||||
elif property_name and hasattr(target_object, property_name):
|
||||
setattr(target_object, property_name, self._color_picker_current_color)
|
||||
|
||||
# 调用回调函数
|
||||
if self._color_picker_callback:
|
||||
self._color_picker_callback(self._color_picker_current_color)
|
||||
|
||||
except Exception as e:
|
||||
print(f"应用颜色失败: {e}")
|
||||
@ -1602,6 +2089,7 @@ class PropertyHelpers:
|
||||
self._font_selector_active = True
|
||||
self._font_selector_target = (target_object, property_name)
|
||||
self._font_selector_current_font = current_font or ""
|
||||
self._font_selector_search_text = ""
|
||||
self._font_selector_callback = callback
|
||||
|
||||
|
||||
@ -1632,7 +2120,10 @@ class PropertyHelpers:
|
||||
imgui.text(f"当前字体: {self._font_selector_current_font or '默认'}")
|
||||
|
||||
# 字体搜索框
|
||||
changed, search_text = imgui.input_text("搜索", "", 256)
|
||||
changed, search_text = imgui.input_text("搜索", self._font_selector_search_text, 256)
|
||||
if changed:
|
||||
self._font_selector_search_text = search_text
|
||||
search_text = self._font_selector_search_text
|
||||
imgui.separator()
|
||||
|
||||
# 字体列表
|
||||
|
||||
@ -63,11 +63,21 @@ class ScriptPanels:
|
||||
|
||||
# 输入框
|
||||
imgui.separator()
|
||||
changed, command = imgui.input_text(">", "", 256)
|
||||
if changed and command:
|
||||
submit, new_command = imgui.input_text(
|
||||
">",
|
||||
self.console_command_input,
|
||||
flags=imgui.InputTextFlags_.enter_returns_true.value,
|
||||
)
|
||||
if new_command != self.console_command_input:
|
||||
self.console_command_input = new_command
|
||||
imgui.same_line()
|
||||
execute_clicked = imgui.button("执行")
|
||||
if (submit or execute_clicked) and self.console_command_input.strip():
|
||||
command = self.console_command_input.strip()
|
||||
self.add_info_message(f"执行命令: {command}")
|
||||
self.console_command_input = ""
|
||||
# TODO: 实现命令执行逻辑
|
||||
|
||||
|
||||
imgui.separator()
|
||||
|
||||
# 视角控制信息
|
||||
|
||||