This commit is contained in:
Hector 2026-02-25 14:27:43 +08:00
parent 26b8e0d93f
commit b7abab4f93
6 changed files with 121 additions and 39 deletions

3
.gitignore vendored
View File

@ -55,3 +55,6 @@ Subjects/test_maintenance_config.json
Resources/models/Women_1.glb
Resources/models/Women_1.glb
Resources/models/Women_1.glb
/venv/
/engine/
/panda3d_imgui-1.1.0-py3-none-any.whl

View File

@ -24,14 +24,14 @@ Size=832,45
Collapsed=0
[Window][工具栏]
Pos=325,20
Size=1228,32
Pos=323,20
Size=1230,32
Collapsed=0
DockId=0x0000000D,0
[Window][场景树]
Pos=0,20
Size=323,634
Size=321,634
Collapsed=0
DockId=0x00000007,0
@ -43,7 +43,7 @@ DockId=0x00000003,0
[Window][控制台]
Pos=0,656
Size=323,353
Size=321,353
Collapsed=0
DockId=0x00000008,0
@ -99,8 +99,8 @@ Size=600,500
Collapsed=0
[Window][资源管理器]
Pos=325,827
Size=1228,182
Pos=323,827
Size=1230,182
Collapsed=0
DockId=0x00000006,0
@ -198,10 +198,10 @@ Collapsed=0
[Docking][Data]
DockSpace ID=0x08BD597D Window=0x1BBC0F80 Pos=0,20 Size=1920,989 Split=X
DockNode ID=0x00000001 Parent=0x08BD597D SizeRef=1553,989 Split=X
DockNode ID=0x00000009 Parent=0x00000001 SizeRef=323,989 Split=Y Selected=0xE0015051
DockNode ID=0x00000009 Parent=0x00000001 SizeRef=321,989 Split=Y Selected=0xE0015051
DockNode ID=0x00000007 Parent=0x00000009 SizeRef=271,634 Selected=0xE0015051
DockNode ID=0x00000008 Parent=0x00000009 SizeRef=271,353 Selected=0x5428E753
DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1228,989 Split=Y
DockNode ID=0x0000000A Parent=0x00000001 SizeRef=1230,989 Split=Y
DockNode ID=0x0000000D Parent=0x0000000A SizeRef=1318,32 HiddenTabBar=1 Selected=0x43A39006
DockNode ID=0x0000000E Parent=0x0000000A SizeRef=1318,955 Split=Y
DockNode ID=0x00000005 Parent=0x0000000E SizeRef=1341,771 CentralNode=1

View File

@ -42,6 +42,7 @@ class ObjectController:
self.virtual_tree_meta = None
self.model = None
self.pick_model = None
self.chunk_node = None # Single chunk node
def bake_ids_and_collect(self, model):
@ -72,6 +73,7 @@ class ObjectController:
self.local_transform_base_positions = {}
self.virtual_tree = None
self.virtual_tree_meta = None
self.pick_model = None
global_id_counter = 0
chunk_key = model.get_name() or "default"
@ -151,6 +153,10 @@ class ObjectController:
self._build_vertex_index(model)
self._init_local_transform_state()
self.build_virtual_hierarchy()
# Keep ID colors only in picking clone to avoid affecting visible shading.
self.pick_model = model.copy_to(NodePath("ssbo_pick_root"))
self._set_uniform_vertex_color(model, 1.0, 1.0, 1.0, 1.0)
t2 = time.time()
print(f"[控制器] Vertex index built in {(t2-t1)*1000:.0f}ms, "
@ -160,6 +166,23 @@ class ObjectController:
self.node_list.sort()
return global_id_counter
def _set_uniform_vertex_color(self, root_np, r, g, b, a):
"""
Force vertex color to a uniform value on visible model to avoid
ID-encoding colors tinting the final render output.
"""
for gn_np in root_np.find_all_matches("**/+GeomNode"):
gnode = gn_np.node()
for gi in range(gnode.get_num_geoms()):
geom = gnode.modify_geom(gi)
vdata = geom.modify_vertex_data()
if not vdata.has_column("color"):
continue
writer = GeomVertexWriter(vdata, InternalName.make("color"))
for row in range(vdata.get_num_rows()):
writer.set_row(row)
writer.set_data4f(r, g, b, a)
def build_virtual_hierarchy(self):
"""Build a readonly virtual tree from node_list path keys."""
root = {

View File

@ -230,7 +230,10 @@ class SSBOEditor:
pick_vert,
pick_frag
)
self.pick_cam.set_scene(self.model)
pick_scene = getattr(self.controller, "pick_model", None) if self.controller else None
if pick_scene is None:
pick_scene = self.model
self.pick_cam.set_scene(pick_scene)
initial_state = NodePath("initial")
initial_state.set_shader(pick_shader, 100)
# Remove global SSBO input, Chunks have their own inputs

View File

@ -608,28 +608,48 @@ class AppActions:
created_any = False
source_nodes = getattr(self, "clipboard_source_nodes", []) or []
for i, node_data in enumerate(self.clipboard):
def _paste_create_node(parent):
created = None
# Copy mode: prefer direct NodePath clone to preserve visual geometry.
if self.clipboard_mode == "copy" and i < len(source_nodes):
source_node = source_nodes[i]
if source_node and not source_node.isEmpty():
try:
created = source_node.copyTo(parent)
if hasattr(self.scene_manager, "_generateUniqueName"):
unique_name = self.scene_manager._generateUniqueName(source_node.getName(), parent)
created.setName(unique_name)
# Preserve model source tags so later cut/paste can rebuild real model.
for _tag in ("model_path", "saved_model_path", "original_path", "file", "element_type"):
if source_node.hasTag(_tag):
created.setTag(_tag, source_node.getTag(_tag))
# Offset slightly so the new node can be seen immediately.
created.setPos(created.getX() + 0.2, created.getY() + 0.2, created.getZ())
except Exception:
created = None
# Fallback: recreate from serialized data.
if not created:
if hasattr(self.scene_manager, "recreateNodeFromData"):
created = self.scene_manager.recreateNodeFromData(node_data, parent)
else:
created = self.scene_manager.deserializeNode(node_data, parent)
return created
new_node = None
if hasattr(self, "command_manager") and self.command_manager:
try:
from core.Command_System import CreateNodeCommand
create_cmd = CreateNodeCommand(_paste_create_node, parent_node)
self.command_manager.execute_command(create_cmd)
new_node = create_cmd.created_node
except Exception as e:
print(f"[Paste] command create failed, fallback direct create: {e}")
new_node = _paste_create_node(parent_node)
else:
new_node = _paste_create_node(parent_node)
# Copy mode: prefer direct NodePath clone to preserve visual geometry.
if self.clipboard_mode == "copy" and i < len(source_nodes):
source_node = source_nodes[i]
if source_node and not source_node.isEmpty():
try:
new_node = source_node.copyTo(parent_node)
if hasattr(self.scene_manager, "_generateUniqueName"):
unique_name = self.scene_manager._generateUniqueName(source_node.getName(), parent_node)
new_node.setName(unique_name)
# Offset slightly so the new node can be seen immediately.
new_node.setPos(new_node.getX() + 0.2, new_node.getY() + 0.2, new_node.getZ())
except Exception:
new_node = None
# Fallback: recreate from serialized data.
if not new_node:
if hasattr(self.scene_manager, "recreateNodeFromData"):
new_node = self.scene_manager.recreateNodeFromData(node_data, parent_node)
else:
new_node = self.scene_manager.deserializeNode(node_data, parent_node)
if new_node:
created_any = True
# Ensure pasted model can be picked by legacy collision fallback.

View File

@ -511,21 +511,54 @@ class EditorPanels:
if not model or model != node or not controller:
return False
node_list = getattr(controller, "node_list", None) or []
if not node_list:
tree_root = controller.get_virtual_hierarchy() if hasattr(controller, "get_virtual_hierarchy") else None
if not tree_root or not tree_root.get("children"):
imgui.text_disabled("(无可用子节点)")
return True
selected_key = getattr(ssbo_editor, "selected_name", None)
for idx, key in enumerate(node_list):
display = controller.display_names.get(key, key.split("/")[-1])
is_selected = (selected_key == key)
if imgui.selectable(f"{display}##ssbo_node_{idx}", is_selected)[0]:
ssbo_editor.select_node(key)
# 与旧系统保持一致选择场景树项时清理LUI选择
for name in sorted(tree_root["children"].keys()):
child = tree_root["children"][name]
self._draw_ssbo_virtual_tree_node(ssbo_editor, child, "ssbo_root")
return True
def _draw_ssbo_virtual_tree_node(self, ssbo_editor, tree_node, unique_id_prefix, depth=0):
"""Recursively draw virtual SSBO hierarchy in scene tree."""
if not tree_node:
return
path = tree_node.get("path", "")
display = tree_node.get("display_name") or tree_node.get("name") or path
leaf_key = tree_node.get("leaf_key")
children = tree_node.get("children", {}) or {}
label = f"{display}##{unique_id_prefix}_{path}"
# Leaf: selectable to trigger SSBO selection.
if not children and leaf_key:
is_selected = (getattr(ssbo_editor, "selected_name", None) == leaf_key)
if imgui.selectable(label, is_selected)[0]:
ssbo_editor.select_node(leaf_key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
return True
return
# Non-leaf: tree node only for hierarchy display.
opened = imgui.tree_node(label)
if opened:
# If this node is also a selectable leaf, render selectable entry first.
if leaf_key:
is_selected = (getattr(ssbo_editor, "selected_name", None) == leaf_key)
if imgui.selectable(f"[节点] {display}##leaf_{unique_id_prefix}_{path}", is_selected)[0]:
ssbo_editor.select_node(leaf_key)
if hasattr(self.app, "lui_manager"):
self.app.lui_manager.selected_index = -1
for child_name in sorted(children.keys()):
self._draw_ssbo_virtual_tree_node(
ssbo_editor,
children[child_name],
unique_id_prefix,
depth + 1,
)
imgui.tree_pop()
def _show_node_context_menu(self, node, name, node_type):
"""显示节点右键菜单"""
self.app._context_menu_node = True